Module: GeneratorHelper

Overview

rubocop:disable Metrics/ModuleLength

Instance Method Summary collapse

Instance Method Details

#absolute_path_relative_to_destination(pathname) ⇒ Object



174
175
176
177
178
179
# File 'lib/generators/react_on_rails/generator_helper.rb', line 174

def absolute_path_relative_to_destination(pathname)
  destination = Pathname.new(destination_root).cleanpath
  pathname.relative_path_from(destination).to_s
rescue ArgumentError
  nil # Signals the caller to fall back to the default path.
end

#add_documentation_reference(message, source) ⇒ Object



66
67
68
# File 'lib/generators/react_on_rails/generator_helper.rb', line 66

def add_documentation_reference(message, source)
  "#{message} \n#{source}"
end

#add_npm_dependencies(packages, dev: false) ⇒ Object

Safe wrapper for package_json operations



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/generators/react_on_rails/generator_helper.rb', line 35

def add_npm_dependencies(packages, dev: false)
  pj = package_json
  return false unless pj

  begin
    result = if dev
               pj.manager.add(packages, type: :dev, exact: true)
             else
               pj.manager.add(packages, exact: true)
             end
    # package_json#add can return nil for successful side-effect operations.
    result != false
  rescue StandardError => e
    say_status :warning, "Could not add packages via package_json gem: #{e.message}", :yellow
    say_status :warning, "Will fall back to direct package manager commands.", :yellow
    false
  end
end

#bundler_flag_given?Boolean

True when the user passed any explicit bundler flag (–rspack/–no-rspack/–webpack/–no-webpack).

Returns:

  • (Boolean)


290
291
292
# File 'lib/generators/react_on_rails/generator_helper.rb', line 290

def bundler_flag_given?
  options.key?(:rspack) || options.key?(:webpack)
end

#component_extension(options) ⇒ Object



79
80
81
# File 'lib/generators/react_on_rails/generator_helper.rb', line 79

def component_extension(options)
  options.typescript? ? "tsx" : "jsx"
end

#configured_shakapacker_relative_path(config_key, default, allow_root: false) ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
# File 'lib/generators/react_on_rails/generator_helper.rb', line 130

def configured_shakapacker_relative_path(config_key, default, allow_root: false)
  config_path = File.join(destination_root, "config/shakapacker.yml")
  return default unless File.exist?(config_path)

  config = parse_shakapacker_yml(config_path)
  configured_path = shakapacker_path_config_value(config, config_key)

  safe_generator_destination_path(configured_path, default:, allow_root:)
rescue Psych::SyntaxError
  default
end

#destination_config_path(path) ⇒ String

Remap a config path from config/webpack/ to config/rspack/ when using rspack. Source templates always live under config/webpack/ (template names are stable); this method handles the destination remapping.

Parameters:

  • path (String)

    relative path, e.g. “config/webpack/serverWebpackConfig.js”

Returns:

  • (String)

    remapped path when rspack, unchanged otherwise



308
309
310
311
312
# File 'lib/generators/react_on_rails/generator_helper.rb', line 308

def destination_config_path(path)
  return path unless using_rspack?

  path.sub(%r{\Aconfig/webpack/}, "config/rspack/")
end

#detect_react_versionString?

Detect the installed React version from package.json Uses VERSION_PARTS_REGEX pattern from VersionChecker for consistency

Returns:

  • (String, nil)

    React version string (e.g., “19.0.3”) or nil if not found/parseable



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/generators/react_on_rails/generator_helper.rb', line 338

def detect_react_version
  pj = package_json
  return nil unless pj

  dependencies = pj.fetch("dependencies", {})
  react_version = dependencies["react"]
  return nil unless react_version

  # Skip non-version strings (workspace:*, file:, link:, http://, etc.)
  return nil if react_version.include?("/") || react_version.start_with?("workspace:")

  # Extract version using the same regex pattern as VersionChecker
  # Handles: "19.0.3", "^19.0.3", "~19.0.3", "19.0.3-beta.1", etc.
  match = react_version.match(/(\d+)\.(\d+)\.(\d+)(?:[-.]([0-9A-Za-z.-]+))?/)
  return nil unless match

  # Return the matched version (without pre-release suffix for comparison)
  "#{match[1]}.#{match[2]}.#{match[3]}"
rescue StandardError
  nil
end

#example_component_source_directory(component_name) ⇒ Object



121
122
123
# File 'lib/generators/react_on_rails/generator_helper.rb', line 121

def example_component_source_directory(component_name)
  File.join(shakapacker_source_path, "src", component_name)
end

#example_component_source_path(component_name) ⇒ Object



125
126
127
128
# File 'lib/generators/react_on_rails/generator_helper.rb', line 125

def example_component_source_path(component_name)
  # Trailing slash is intentional: this value is only for generated demo file hints.
  "#{example_component_source_directory(component_name)}/"
end

#explicit_bundler_choiceObject

Resolve the explicit bundler flags into a single choice.

–rspack selects Rspack; –no-rspack and –webpack select Webpack (–webpack is a friendly alias for –no-rspack, and the auto-generated –no-webpack mirrors –rspack). Returns true for Rspack, false for Webpack, or nil when no bundler flag was passed (so the caller falls back to rspack_bundler_default).

IMPORTANT: this relies on Thor NOT including a nil-defaulted option in the hash when the flag is absent — options.key?(:rspack)/(:webpack) is true only when the user passed that flag. Re-adding ‘default:` to either class_option would make the key always present and break both the “no flag given” fallback and the conflict detection here. (Thor’s omit-when-no-default behavior verified against Thor 1.5.0; see Gemfile.lock.)

Passing contradictory flags (e.g. –rspack –webpack) raises a Thor::Error.



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

def explicit_bundler_choice
  choices = []
  choices << options[:rspack] if options.key?(:rspack)
  # --webpack means "use Webpack" (rspack = false); --no-webpack means "use Rspack".
  # Name the inverted webpack flag so the rspack-boolean intent reads directly.
  rspack_via_webpack_flag = !options[:webpack]
  choices << rspack_via_webpack_flag if options.key?(:webpack)
  return nil if choices.empty?

  if choices.uniq.length > 1
    raise Thor::Error,
          "Conflicting bundler flags: pass either Rspack (--rspack) or Webpack " \
          "(--webpack / --no-rspack), not both."
  end

  choices.first
end

#gem_in_lockfile?(gem_name) ⇒ Boolean

Check if a gem is present in Gemfile.lock Always checks the target app’s Gemfile.lock, not inherited BUNDLE_GEMFILE See: github.com/shakacode/react_on_rails/issues/2287

Parameters:

  • gem_name (String)

    Name of the gem to check

Returns:

  • (Boolean)

    true if the gem is in Gemfile.lock



191
192
193
194
195
196
# File 'lib/generators/react_on_rails/generator_helper.rb', line 191

def gem_in_lockfile?(gem_name)
  File.file?("Gemfile.lock") &&
    File.foreach("Gemfile.lock").any? { |line| line.match?(/^\s{4}#{Regexp.escape(gem_name)}\s\(/) }
rescue StandardError
  false
end

#package_jsonObject



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/generators/react_on_rails/generator_helper.rb', line 15

def package_json
  # Lazy load package_json gem only when actually needed for dependency management

  require "package_json" unless defined?(PackageJson)
  @package_json ||= PackageJson.read
rescue LoadError
  unless @package_json_unavailable_warned
    say_status :warning, "package_json gem not available. This is expected before Shakapacker installation.", :yellow
    say_status :warning, "Dependencies will be installed using the default package manager after Shakapacker setup.",
               :yellow
    @package_json_unavailable_warned = true
  end
  nil
rescue StandardError => e
  say_status :warning, "Could not read package.json: #{e.message}", :yellow
  say_status :warning, "This is normal before Shakapacker creates the package.json file.", :yellow
  nil
end


70
71
72
73
74
75
76
77
# File 'lib/generators/react_on_rails/generator_helper.rb', line 70

def print_generator_messages
  # GeneratorMessages stores pre-colored strings, so we strip ANSI manually for --no-color output.
  no_color = !shell.is_a?(Thor::Shell::Color)
  GeneratorMessages.messages.each do |message|
    say(no_color ? message.to_s.gsub(/\e\[[0-9;]*m/, "") : message)
    say "" # Blank line after each message for readability
  end
end

#pro_gem_installed?Boolean

Check if React on Rails Pro gem is installed (real state — never “scheduled to be installed”).

Detection priority:

  1. Gem.loaded_specs - gem is loaded in current Ruby process (most reliable)

  2. Gemfile.lock - gem is resolved and installed

Use #pro_gem_install_deferred? for the broader “present, or will be installed by this generator run” meaning. Use #invalidate_pro_gem_installed_cache! after an operation that changes real state (e.g., bundle add) so the next call re-reads the lockfile.

Returns:

  • (Boolean)

    true if react_on_rails_pro gem is installed



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

def pro_gem_installed?
  return @pro_gem_installed if defined?(@pro_gem_installed)

  @pro_gem_installed = Gem.loaded_specs.key?("react_on_rails_pro") || gem_in_lockfile?("react_on_rails_pro")
end

#relative_stylesheet_import_path(entry_path, filename: "application.css") ⇒ Object

Raises:

  • (ArgumentError)


110
111
112
113
114
115
116
117
118
119
# File 'lib/generators/react_on_rails/generator_helper.rb', line 110

def relative_stylesheet_import_path(entry_path, filename: "application.css")
  # InstallGenerator copies the final Shakapacker config before path-dependent demo files are generated.
  safe_entry_path = safe_generator_destination_path(entry_path, default: nil)
  raise ArgumentError, "entry_path must stay inside the generator destination" if safe_entry_path.nil?

  entry_dir = Pathname.new(File.join(destination_root, safe_entry_path)).dirname
  stylesheet = Pathname.new(File.join(destination_root, shakapacker_stylesheet_path(filename)))

  stylesheet.relative_path_from(entry_dir).to_s
end

#resolve_server_client_or_both_pathString?

Resolve the path to ServerClientOrBoth.js, handling the legacy name. Old installs may still use generateWebpackConfigs.js; this renames it and updates references in environment configs so downstream transforms can rely on the canonical name.

Returns:

  • (String, nil)

    relative config path, or nil if neither file exists



416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
# File 'lib/generators/react_on_rails/generator_helper.rb', line 416

def resolve_server_client_or_both_path
  new_path = destination_config_path("config/webpack/ServerClientOrBoth.js")
  old_path = destination_config_path("config/webpack/generateWebpackConfigs.js")
  full_new = File.join(destination_root, new_path)
  full_old = File.join(destination_root, old_path)

  if File.exist?(full_new)
    new_path
  elsif File.exist?(full_old)
    FileUtils.mv(full_old, full_new)
    %w[development.js production.js test.js].each do |env_file|
      env_path = destination_config_path("config/webpack/#{env_file}")
      if File.exist?(File.join(destination_root, env_path))
        gsub_file(env_path, /generateWebpackConfigs/, "ServerClientOrBoth")
      end
    end
    new_path
  end
end

#root_route_present?(routes_path = File.join(destination_root, "config/routes.rb")) ⇒ Boolean

Detect whether config/routes.rb defines any non-commented root route.

Parameters:

  • routes_path (String) (defaults to: File.join(destination_root, "config/routes.rb"))

    absolute path to routes.rb

Returns:

  • (Boolean)

    true when a root route exists



58
59
60
61
62
63
64
# File 'lib/generators/react_on_rails/generator_helper.rb', line 58

def root_route_present?(routes_path = File.join(destination_root, "config/routes.rb"))
  return false unless File.file?(routes_path)

  File.foreach(routes_path).any? do |line|
    !line.match?(/^\s*#/) && line.match?(/^\s*root\b/)
  end
end

#rsc_plugin_class_nameString

RSC client-manifest plugin class name for the active bundler. Rspack uses the native ‘RSCRspackPlugin`; webpack uses `RSCWebpackPlugin`. Both expose the same `{ isServer, clientReferences }` API and emit the same manifest schema, so only the import path and class name differ. Shared by the base webpack-config templates and the standalone RSC migration so both paths scaffold the bundler-correct plugin from one source of truth.

Returns:

  • (String)

    “RSCRspackPlugin” when rspack, “RSCWebpackPlugin” otherwise



322
323
324
# File 'lib/generators/react_on_rails/generator_helper.rb', line 322

def rsc_plugin_class_name
  using_rspack? ? "RSCRspackPlugin" : "RSCWebpackPlugin"
end

#rsc_plugin_import_pathString

‘react-on-rails-rsc` subpath that exports #rsc_plugin_class_name.

Returns:

  • (String)

    “react-on-rails-rsc/RspackPlugin” when rspack, “react-on-rails-rsc/WebpackPlugin” otherwise



330
331
332
# File 'lib/generators/react_on_rails/generator_helper.rb', line 330

def rsc_plugin_import_path
  using_rspack? ? "react-on-rails-rsc/RspackPlugin" : "react-on-rails-rsc/WebpackPlugin"
end

#rspack_bundler_defaultObject

Bundler to use when no explicit bundler flag was passed. Default (standalone generators like RscGenerator/ProGenerator): respect the existing project’s shakapacker.yml and never impose a bundler. InstallGenerator/BaseGenerator override this to default fresh installs to Rspack.



298
299
300
# File 'lib/generators/react_on_rails/generator_helper.rb', line 298

def rspack_bundler_default
  rspack_configured_in_project?
end

#safe_generator_destination_path(path, default:, allow_root: false) ⇒ Object



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/generators/react_on_rails/generator_helper.rb', line 153

def safe_generator_destination_path(path, default:, allow_root: false)
  candidate = path.to_s.strip
  return default if candidate.empty?

  pathname = Pathname.new(candidate).cleanpath
  # Shakapacker uses "/" to mean entrypoints live directly under source_path.
  return "" if allow_root && pathname.to_s == "/"

  relative_path = if pathname.absolute?
                    absolute_path_relative_to_destination(pathname)
                  else
                    pathname.to_s
                  end

  return default if unsafe_generator_destination_path?(relative_path)

  relative_path
rescue ArgumentError # Pathname.new raises on null bytes in path strings.
  default
end

#shakapacker_entrypoint_path(filename) ⇒ Object

Raises:

  • (ArgumentError)


97
98
99
100
101
102
103
# File 'lib/generators/react_on_rails/generator_helper.rb', line 97

def shakapacker_entrypoint_path(filename)
  filename = filename.to_s
  raise ArgumentError, "filename must be present" if filename.empty?

  entry_dir = shakapacker_source_entry_path # "" means entrypoints live directly under source_path.
  File.join(*[shakapacker_source_path, entry_dir, filename].reject(&:empty?))
end

#shakapacker_path_config_value(config, config_key) ⇒ Object



142
143
144
145
146
147
148
149
150
151
# File 'lib/generators/react_on_rails/generator_helper.rb', line 142

def shakapacker_path_config_value(config, config_key)
  # Generators run in the development context, so prefer that section before falling back to shared defaults.
  %w[development default].each do |section_name|
    section = shakapacker_config_section(config, section_name)
    value = shakapacker_config_value(section, config_key)
    return value unless value.to_s.strip.empty?
  end

  nil
end

#shakapacker_source_entry_pathObject



89
90
91
92
93
94
95
# File 'lib/generators/react_on_rails/generator_helper.rb', line 89

def shakapacker_source_entry_path
  @shakapacker_source_entry_path ||= configured_shakapacker_relative_path(
    "source_entry_path",
    DEFAULT_SHAKAPACKER_SOURCE_ENTRY_PATH,
    allow_root: true
  )
end

#shakapacker_source_pathObject



83
84
85
86
87
# File 'lib/generators/react_on_rails/generator_helper.rb', line 83

def shakapacker_source_path
  # These helpers memoize config-backed paths. Install generators must copy or
  # overwrite config/shakapacker.yml before any path-dependent copy action runs.
  @shakapacker_source_path ||= configured_shakapacker_relative_path("source_path", DEFAULT_SHAKAPACKER_SOURCE_PATH)
end

#shakapacker_stylesheet_path(filename) ⇒ Object



105
106
107
108
# File 'lib/generators/react_on_rails/generator_helper.rb', line 105

def shakapacker_stylesheet_path(filename)
  # "stylesheets" is a generated demo convention, not a Shakapacker config key.
  File.join(shakapacker_source_path, "stylesheets", filename)
end

#shakapacker_version_9_or_higher?Boolean

Note:

Default behavior: Returns true when Shakapacker is not yet installed Rationale: During fresh installations, we optimistically assume users will install the latest Shakapacker version. This ensures new projects get best-practice configs. If users later install an older version, the generated webpack config includes fallback logic (e.g., ‘config.privateOutputPath || hardcodedPath`) that prevents breakage, and validation warnings guide them to fix any misconfigurations.

Check if Shakapacker 9.0 or higher is available Returns true if Shakapacker >= 9.0, false otherwise

This method is used during code generation to determine which configuration patterns to use in generated files (e.g., config.privateOutputPath vs hardcoded paths).

Returns:

  • (Boolean)

    true if Shakapacker 9.0+ is available or likely to be installed



374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/generators/react_on_rails/generator_helper.rb', line 374

def shakapacker_version_9_or_higher?
  return @shakapacker_version_9_or_higher if defined?(@shakapacker_version_9_or_higher)

  @shakapacker_version_9_or_higher = begin
    # If Shakapacker is not available yet (fresh install), default to true
    # since we're likely installing the latest version
    return true unless defined?(ReactOnRails::PackerUtils)

    ReactOnRails::PackerUtils.shakapacker_version_requirement_met?("9.0.0")
  rescue StandardError
    # If we can't determine version, assume latest
    true
  end
end

#unsafe_generator_destination_path?(path) ⇒ Boolean

Returns:

  • (Boolean)


181
182
183
# File 'lib/generators/react_on_rails/generator_helper.rb', line 181

def unsafe_generator_destination_path?(path)
  path.nil? || path == "." || path == ".." || path.start_with?("../")
end

#use_pro?Boolean

Check if Pro features should be enabled. Returns true if –pro or –rsc is set (RSC implies Pro).

Returns:

  • (Boolean)

    true if Pro setup should be included



219
220
221
# File 'lib/generators/react_on_rails/generator_helper.rb', line 219

def use_pro?
  options[:pro] || options[:rsc]
end

#use_rsc?Boolean

Check if RSC (React Server Components) should be enabled. Returns true if –rsc is set.

Returns:

  • (Boolean)

    true if RSC setup should be included



227
228
229
# File 'lib/generators/react_on_rails/generator_helper.rb', line 227

def use_rsc?
  options[:rsc]
end

#use_tailwind?Boolean

Check if Tailwind CSS should be installed and wired into the generated example.

Returns:

  • (Boolean)

    true if –tailwind is set



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

def use_tailwind?
  options[:tailwind]
end

#using_rspack?Boolean

Determine if the project is using rspack as the bundler.

Detection priority:

  1. Explicit –rspack option (most reliable during fresh installs)

  2. config/shakapacker.yml assets_bundler setting (for standalone generators like ‘rails g react_on_rails:rsc` on an existing rspack project)

Returns:

  • (Boolean)

    true if rspack is the configured bundler



246
247
248
249
250
251
252
253
254
# File 'lib/generators/react_on_rails/generator_helper.rb', line 246

def using_rspack?
  return @using_rspack if defined?(@using_rspack)

  # An explicit bundler flag always wins. When none was passed (or the generator doesn't
  # declare the flags, e.g. RscGenerator/ProGenerator), fall back to the bundler default,
  # which each generator defines for its own context.
  explicit = explicit_bundler_choice
  @using_rspack = explicit.nil? ? rspack_bundler_default : explicit
end

#using_swc?Boolean

Note:

This method is used to determine whether to install SWC dependencies (@swc/core, swc-loader) instead of Babel dependencies during generation.

Note:

Caching: The result is memoized for the lifetime of the generator instance. If shakapacker.yml changes during generator execution (unlikely), the cached value will not update. This is acceptable since generators run quickly.

Check if SWC is configured as the JavaScript transpiler in shakapacker.yml

Detection logic:

  1. If shakapacker.yml exists and specifies javascript_transpiler: parse it

  2. For Shakapacker 9.3.0+, SWC is the default if not specified

  3. Returns true for fresh installations (SWC is recommended default)

Returns:

  • (Boolean)

    true if SWC is configured or should be used by default



404
405
406
407
408
# File 'lib/generators/react_on_rails/generator_helper.rb', line 404

def using_swc?
  return @using_swc if defined?(@using_swc)

  @using_swc = detect_swc_configuration
end