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

.default_troubleshooting_sectionObject



456
457
458
459
460
461
462
463
464
# File 'lib/react_on_rails/utils.rb', line 456

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)



290
291
292
293
# File 'lib/react_on_rails/utils.rb', line 290

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



263
264
265
266
267
268
# File 'lib/react_on_rails/utils.rb', line 263

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)


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

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

.gem_available?(name) ⇒ Boolean

Returns:

  • (Boolean)


202
203
204
205
206
207
208
209
210
211
212
# File 'lib/react_on_rails/utils.rb', line 202

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



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

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



428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
# File 'lib/react_on_rails/utils.rb', line 428

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



362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'lib/react_on_rails/utils.rb', line 362

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



385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'lib/react_on_rails/utils.rb', line 385

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



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

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



270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/react_on_rails/utils.rb', line 270

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



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

def self.public_bundles_full_path
  ReactOnRails::PackerUtils.packer_public_output_path
end

.rails_version_less_than(version) ⇒ Object



164
165
166
167
168
169
170
171
172
# File 'lib/react_on_rails/utils.rb', line 164

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



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

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



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

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)


237
238
239
240
241
# File 'lib/react_on_rails/utils.rb', line 237

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



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/react_on_rails/utils.rb', line 247

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



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

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)


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

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