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



115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/react_on_rails/utils.rb', line 115

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



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

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)



332
333
334
335
# File 'lib/react_on_rails/utils.rb', line 332

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



305
306
307
308
309
310
# File 'lib/react_on_rails/utils.rb', line 305

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)


285
286
287
# File 'lib/react_on_rails/utils.rb', line 285

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

.gem_available?(name) ⇒ Boolean

Returns:

  • (Boolean)


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

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



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

def self.generated_assets_full_path
  public_bundles_full_path
end

.immediate_hydration_pro_license_warning(name, type = "Component") ⇒ Object



17
18
19
20
21
# File 'lib/react_on_rails/utils.rb', line 17

def self.immediate_hydration_pro_license_warning(name, type = "Component")
  "[REACT ON RAILS] Warning: immediate_hydration: true requires a React on Rails Pro license.\n" \
    "#{type} '#{name}' will fall back to standard hydration behavior.\n" \
    "Visit https://www.shakacode.com/react-on-rails-pro/ for licensing information."
end

.invoke_and_exit_if_failed(cmd, failure_message) ⇒ Object

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



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/react_on_rails/utils.rb', line 89

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

    puts wrap_message(msg)
    puts ""
    puts default_troubleshooting_section

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

.normalize_immediate_hydration(value, name, type = "Component") ⇒ Boolean

Normalizes the immediate_hydration option value, enforcing Pro license requirements. Returns the normalized boolean value for immediate_hydration.

Logic:

  • Validates that value is true, false, or nil

  • If value is explicitly true (boolean) and no Pro license: warn and return false

  • If value is nil: return true for Pro users, false for non-Pro users

  • Otherwise: return the value as-is (allows explicit false to work)

Parameters:

  • value (Boolean, nil)

    The immediate_hydration option value

  • name (String)

    The name of the component/store (for warning messages)

  • type (String) (defaults to: "Component")

    The type (“Component” or “Store”) for warning messages

Returns:

  • (Boolean)

    The normalized immediate_hydration value

Raises:

  • (ArgumentError)

    If value is not a boolean or nil



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/react_on_rails/utils.rb', line 37

def self.normalize_immediate_hydration(value, name, type = "Component")
  # Type validation: only accept boolean or nil
  unless [true, false, nil].include?(value)
    raise ArgumentError,
          "[REACT ON RAILS] immediate_hydration must be true, false, or nil. Got: #{value.inspect} (#{value.class})"
  end

  # Strict equality check: only trigger warning for explicit boolean true
  if value == true && !react_on_rails_pro?
    Rails.logger.warn immediate_hydration_pro_license_warning(name, type)
    return false
  end

  # If nil, default based on Pro license status
  return react_on_rails_pro? if value.nil?

  # Return explicit value (including false)
  value
end

.object_to_boolean(value) ⇒ Object



80
81
82
# File 'lib/react_on_rails/utils.rb', line 80

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



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
# File 'lib/react_on_rails/utils.rb', line 404

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



427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
# File 'lib/react_on_rails/utils.rb', line 427

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



217
218
219
# File 'lib/react_on_rails/utils.rb', line 217

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



312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/react_on_rails/utils.rb', line 312

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



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

def self.public_bundles_full_path
  ReactOnRails::PackerUtils.packer_public_output_path
end

.rails_version_less_than(version) ⇒ Object



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

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 React on Rails Pro is installed and licensed. This method validates the license and will raise an exception if invalid.

Returns:

  • (Boolean)

    true if Pro is available with valid license

Raises:

  • (ReactOnRailsPro::Error)

    if license is invalid



256
257
258
259
260
261
262
263
264
# File 'lib/react_on_rails/utils.rb', line 256

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

  @react_on_rails_pro = begin
    return false unless gem_available?("react_on_rails_pro")

    ReactOnRailsPro::Utils.validated_license_data!.present?
  end
end

.react_on_rails_pro_versionObject

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



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

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)


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

def self.rsc_support_enabled?
  return false unless react_on_rails_pro?

  ReactOnRailsPro::Utils.rsc_support_enabled?
end

.running_on_windows?Boolean

Returns:

  • (Boolean)


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

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

.server_bundle_js_file_pathObject



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

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)


111
112
113
# File 'lib/react_on_rails/utils.rb', line 111

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

.server_rendering_is_enabled?Boolean

Returns:

  • (Boolean)


84
85
86
# File 'lib/react_on_rails/utils.rb', line 84

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

.smart_trim(str, max_length = 1000) ⇒ Object



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/react_on_rails/utils.rb', line 289

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



221
222
223
# File 'lib/react_on_rails/utils.rb', line 221

def self.source_path
  ReactOnRails::PackerUtils.packer_source_path
end

.truthy_presence(obj) ⇒ Object



59
60
61
62
63
64
65
# File 'lib/react_on_rails/utils.rb', line 59

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)


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

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.



69
70
71
72
73
74
75
76
77
78
# File 'lib/react_on_rails/utils.rb', line 69

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