Module: ReactOnRails::Utils

Defined in:
lib/react_on_rails/utils.rb,
sig/react_on_rails/utils.rbs

Defined Under Namespace

Modules: Required

Constant Summary collapse

TRUNCATION_FILLER =

Returns:

  • (String)
"\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) ⇒ String

Parameters:

  • bundle_name (String)

Returns:

  • (String)


74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/react_on_rails/utils.rb', line 74

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)


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

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_sectionString

Returns:

  • (String)


447
448
449
450
451
452
453
454
455
# File 'lib/react_on_rails/utils.rb', line 447

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)



281
282
283
284
# File 'lib/react_on_rails/utils.rb', line 281

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) ⇒ Time?

Parameters:

Returns:

  • (Time, nil)


254
255
256
257
258
259
# File 'lib/react_on_rails/utils.rb', line 254

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)


234
235
236
# File 'lib/react_on_rails/utils.rb', line 234

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

.gem_available?(name) ⇒ Boolean

Parameters:

  • name (String)

Returns:

  • (Boolean)


193
194
195
196
197
198
199
200
201
202
203
# File 'lib/react_on_rails/utils.rb', line 193

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_pathString

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

Returns:

  • (String)


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

def self.generated_assets_full_path
  public_bundles_full_path
end

.invoke_and_exit_if_failed(cmd, failure_message) ⇒ Array[String]

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

Parameters:

  • cmd (String)
  • failure_message (String)

Returns:



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/react_on_rails/utils.rb', line 45

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



419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# File 'lib/react_on_rails/utils.rb', line 419

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) ⇒ Boolean

Parameters:

  • value (Object)

Returns:

  • (Boolean)


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")



353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/react_on_rails/utils.rb', line 353

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")



376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
# File 'lib/react_on_rails/utils.rb', line 376

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



171
172
173
# File 'lib/react_on_rails/utils.rb', line 171

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:) ⇒ void

This method returns an undefined value.

Parameters:

  • file: (String)
  • text_to_prepend: (String)
  • regex: (Regexp)


261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/react_on_rails/utils.rb', line 261

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_pathString

Returns:

  • (String)


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

def self.public_bundles_full_path
  ReactOnRails::PackerUtils.packer_public_output_path
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



209
210
211
212
213
# File 'lib/react_on_rails/utils.rb', line 209

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_versionString?

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

Returns:

  • (String, nil)


216
217
218
219
220
221
222
223
224
# File 'lib/react_on_rails/utils.rb', line 216

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)


228
229
230
231
232
# File 'lib/react_on_rails/utils.rb', line 228

def self.rsc_support_enabled?
  return false unless react_on_rails_pro?

  ReactOnRailsPro::Utils.rsc_support_enabled?
end

.running_on_windows?Boolean

Returns:

  • (Boolean)


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

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

.server_bundle_js_file_pathString

Returns:

  • (String)


149
150
151
152
153
154
# File 'lib/react_on_rails/utils.rb', line 149

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)


70
71
72
# File 'lib/react_on_rails/utils.rb', line 70

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

.smart_trim(str, max_length = 1000) ⇒ String

Parameters:

  • str (String)
  • max_length (Integer) (defaults to: 1000)

Returns:

  • (String)


238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/react_on_rails/utils.rb', line 238

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_pathString

Returns:

  • (String)


175
176
177
# File 'lib/react_on_rails/utils.rb', line 175

def self.source_path
  ReactOnRails::PackerUtils.packer_source_path
end

.truthy_presence(obj) ⇒ Object

Parameters:

  • obj (Object)

Returns:

  • (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)


179
180
181
182
# File 'lib/react_on_rails/utils.rb', line 179

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) ⇒ String

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

Parameters:

  • msg (String)
  • color (Symbol) (defaults to: :red)

Returns:

  • (String)


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