module ReactOnRails::Utils
def self.bundle_js_file_path(bundle_name)
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
def self.bundle_js_file_path_with_packer(bundle_name)
def self.bundle_js_file_path_with_packer(bundle_name) server_bundle?(bundle_name) ls.configuration oot || "." and server_bundle_output_path is configured, return that path directly && config.server_bundle_output_path.present? ndle_path = File.expand_path(File.join(root_path, config.server_bundle_output_path, bundle_name)) to public directory if enforce_private_server_bundles is enabled _private_server_bundles || File.exist?(private_server_bundle_path) server_bundle_path up for all bundles kerUtils.bundle_js_uri_from_packer(bundle_name) :Manifest::MissingEntryError nifest_entry(bundle_name, is_server_bundle)
def self.default_troubleshooting_section
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
def self.detect_package_manager
-
(Symbol)- The package manager symbol (:npm, :yarn, :pnpm, :bun)
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
def self.detect_package_manager_from_lock_files
def self.detect_package_manager_from_lock_files e.exist?(File.join(root, "yarn.lock")) e.exist?(File.join(root, "pnpm-lock.yaml")) .exist?(File.join(root, "bun.lockb")) .exist?(File.join(root, "package-lock.json"))
def self.detect_package_manager_from_package_json
def self.detect_package_manager_from_package_json File.join(Rails.root, ReactOnRails.configuration.node_modules_location, "package.json") ile.exist?(package_json_path) JSON.parse(File.read(package_json_path)) ackage_json_data["packageManager"] ckage_json_data["packageManager"] ame from strings like "yarn@3.6.0" or "pnpm@8.0.0" ger_string.split("@").first if %w[npm yarn pnpm bun].include?(manager_name)
def self.find_most_recent_mtime(files)
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
def self.full_text_errors_enabled?
def self.full_text_errors_enabled? ENV["FULL_TEXT_ERRORS"] == "true" end
def self.gem_available?(name)
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
def self.generated_assets_full_path
def self.generated_assets_full_path public_bundles_full_path end
def self.handle_missing_manifest_entry(bundle_name, is_server_bundle)
def self.handle_missing_manifest_entry(bundle_name, is_server_bundle) ls.configuration oot || "." s with server_bundle_output_path configured, use that && config.server_bundle_output_path.present? [File.expand_path(File.join(root_path, config.server_bundle_output_path, bundle_name))] orce_private_server_bundles << File.expand_path(File.join(ReactOnRails::PackerUtils.packer_public_output_path, bundle_name)) ach do |path| File.exist?(path) paths.first s and server bundles without special config, use packer's public path environment-specific path configured in shakapacker.yml le.join(ReactOnRails::PackerUtils.packer_public_output_path, bundle_name))
def self.immediate_hydration_pro_license_warning(name, type = "Component")
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
def self.invoke_and_exit_if_failed(cmd, failure_message)
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
def self.normalize_immediate_hydration(value, name, type = "Component")
-
(ArgumentError)- If value is not a boolean or nil
Returns:
-
(Boolean)- The normalized immediate_hydration value
Parameters:
-
type(String) -- The type ("Component" or "Store") for warning messages -
name(String) -- The name of the component/store (for warning messages) -
value(Boolean, nil) -- The immediate_hydration option value
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
def self.object_to_boolean(value)
def self.object_to_boolean(value) [true, "true", "yes", 1, "1", "t"].include?(value.instance_of?(String) ? value.downcase : value) end
def self.package_manager_install_exact_command(package_name, version)
-
(String)- The command to run (e.g., "yarn add react-on-rails@16.0.0 --exact")
Parameters:
-
version(String) -- The exact version to install -
package_name(String) -- The name of the package to install
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
def self.package_manager_remove_command(package_name)
-
(String)- The command to run (e.g., "yarn remove react-on-rails")
Parameters:
-
package_name(String) -- The name of the package to remove
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
def self.prepend_cd_node_modules_directory(cmd)
def self.prepend_cd_node_modules_directory(cmd) "cd \"#{ReactOnRails.configuration.node_modules_location}\" && #{cmd}" end
def self.prepend_to_file_if_text_not_present(file:, text_to_prepend:, regex:)
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
def self.public_bundles_full_path
def self.public_bundles_full_path ReactOnRails::PackerUtils.packer_public_output_path end
def self.rails_version_less_than(version)
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
def self.react_on_rails_pro?
-
(ReactOnRailsPro::Error)- if license is invalid
Returns:
-
(Boolean)- true if Pro is available with valid license
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
def self.react_on_rails_pro_version
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
def self.rsc_support_enabled?
RSC support detection has been moved to React on Rails Pro
def self.rsc_support_enabled? return false unless react_on_rails_pro? ReactOnRailsPro::Utils.rsc_support_enabled? end
def self.running_on_windows?
def self.running_on_windows? (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil end
def self.server_bundle?(bundle_name)
def self.server_bundle?(bundle_name) ls.configuration le_name == config.server_bundle_js_file rations if Pro is available ro? tOnRailsPro.configuration ndle_name == pro_config.rsc_bundle_js_file || ndle_name == pro_config.react_server_client_manifest_file
def self.server_bundle_js_file_path
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
def self.server_bundle_path_is_http?
def self.server_bundle_path_is_http? server_bundle_js_file_path =~ %r{https?://} end
def self.server_rendering_is_enabled?
def self.server_rendering_is_enabled? ReactOnRails.configuration.server_bundle_js_file.present? end
def self.smart_trim(str, max_length = 1000)
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
def self.source_path
def self.source_path ReactOnRails::PackerUtils.packer_source_path end
def self.truthy_presence(obj)
https://forum.shakacode.com/t/yak-of-the-week-ruby-2-4-pathname-empty-changed-to-look-at-file-size/901
def self.truthy_presence(obj) if obj.nil? || obj == false nil else obj end end
def self.using_packer_source_path_is_not_defined_and_custom_node_modules?
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
def self.validate_package_command_inputs!(package_name, version)
-
(ReactOnRails::Error)- if inputs contain potentially unsafe characters
Parameters:
-
version(String) -- The version to validate -
package_name(String) -- The package name to validate
def self.validate_package_command_inputs!(package_name, version) me!(package_name) :Error, "version cannot be nil" if version.nil? :Error, "version cannot be empty" if version.to_s.strip.empty? r versions and common npm version patterns 3, 1.2.3-beta.1, 1.2.3-alpha, etc. atch?(/\A[a-z0-9][a-z0-9._-]*\z/i) :Error, "Invalid version: #{version.inspect}. " \ "Versions must contain only alphanumeric characters, dots, hyphens, and underscores."
def self.validate_package_name!(package_name)
-
(ReactOnRails::Error)- if package_name contains potentially unsafe characters
Parameters:
-
package_name(String) -- The package name to validate
def self.validate_package_name!(package_name) :Error, "package_name cannot be nil" if package_name.nil? :Error, "package_name cannot be empty" if package_name.to_s.strip.empty? ackage names: alphanumeric, hyphens, underscores, dots, slashes (for scoped packages) ub.com/npm/validate-npm-package-name ame.match?(%r{\A[@a-z0-9][a-z0-9._/-]*\z}i) :Error, "Invalid package name: #{package_name.inspect}. " \ "Package names must contain only alphanumeric characters, " \ "hyphens, underscores, dots, and slashes (for scoped packages)."
def self.wrap_message(msg, color = :red)
Wraps message and makes it colored.
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