react_on_rails 16.6.0 → 16.7.0.rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/Gemfile.development_dependencies +2 -2
  4. data/Gemfile.lock +2 -14
  5. data/Rakefile +0 -6
  6. data/Steepfile +4 -0
  7. data/lib/generators/react_on_rails/base_generator.rb +4 -4
  8. data/lib/generators/react_on_rails/demo_page_config.rb +3 -3
  9. data/lib/generators/react_on_rails/dev_tests_generator.rb +1 -1
  10. data/lib/generators/react_on_rails/generator_helper.rb +6 -65
  11. data/lib/generators/react_on_rails/generator_messages/ci_section.rb +42 -0
  12. data/lib/generators/react_on_rails/generator_messages/package_manager_detection.rb +194 -0
  13. data/lib/generators/react_on_rails/generator_messages/shakapacker_status_section.rb +61 -0
  14. data/lib/generators/react_on_rails/generator_messages.rb +22 -79
  15. data/lib/generators/react_on_rails/install_generator.rb +243 -28
  16. data/lib/generators/react_on_rails/js_dependency_manager.rb +7 -4
  17. data/lib/generators/react_on_rails/pro/USAGE +1 -1
  18. data/lib/generators/react_on_rails/pro_generator.rb +206 -183
  19. data/lib/generators/react_on_rails/pro_setup.rb +102 -26
  20. data/lib/generators/react_on_rails/react_with_redux_generator.rb +3 -2
  21. data/lib/generators/react_on_rails/templates/base/base/.env.example +25 -0
  22. data/lib/generators/react_on_rails/templates/base/base/.github/workflows/ci.yml.tt +86 -0
  23. data/lib/generators/react_on_rails/templates/base/base/Procfile.dev +4 -3
  24. data/lib/generators/react_on_rails/templates/base/base/babel.config.js.tt +1 -1
  25. data/lib/generators/react_on_rails/templates/base/base/bin/switch-bundler +2 -2
  26. data/lib/generators/react_on_rails/templates/base/base/config/webpack/ServerClientOrBoth.js.tt +1 -1
  27. data/lib/generators/react_on_rails/templates/base/base/config/webpack/clientWebpackConfig.js.tt +1 -1
  28. data/lib/generators/react_on_rails/templates/base/base/config/webpack/commonWebpackConfig.js.tt +2 -2
  29. data/lib/generators/react_on_rails/templates/base/base/config/webpack/development.js.tt +1 -1
  30. data/lib/generators/react_on_rails/templates/base/base/config/webpack/production.js.tt +1 -1
  31. data/lib/generators/react_on_rails/templates/base/base/config/webpack/serverWebpackConfig.js.tt +6 -5
  32. data/lib/generators/react_on_rails/templates/base/base/config/webpack/test.js.tt +1 -1
  33. data/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt +1 -1
  34. data/lib/generators/react_on_rails/templates/pro/base/{client → renderer}/node-renderer.js +1 -0
  35. data/lib/react_on_rails/config_path_resolver.rb +101 -4
  36. data/lib/react_on_rails/configuration.rb +22 -0
  37. data/lib/react_on_rails/dev/file_manager.rb +135 -8
  38. data/lib/react_on_rails/dev/port_selector.rb +259 -7
  39. data/lib/react_on_rails/dev/process_manager.rb +29 -2
  40. data/lib/react_on_rails/dev/server_manager.rb +607 -39
  41. data/lib/react_on_rails/doctor.rb +513 -45
  42. data/lib/react_on_rails/helper.rb +3 -11
  43. data/lib/react_on_rails/js_code_builder.rb +66 -0
  44. data/lib/react_on_rails/length_prefixed_parser.rb +142 -0
  45. data/lib/react_on_rails/packs_generator.rb +65 -12
  46. data/lib/react_on_rails/pro_migration.rb +175 -0
  47. data/lib/react_on_rails/render_request.rb +74 -0
  48. data/lib/react_on_rails/rendering_strategy/exec_js_strategy.rb +29 -0
  49. data/lib/react_on_rails/rendering_strategy.rb +44 -0
  50. data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +33 -22
  51. data/lib/react_on_rails/system_checker.rb +44 -23
  52. data/lib/react_on_rails/utils.rb +5 -0
  53. data/lib/react_on_rails/version.rb +1 -1
  54. data/lib/react_on_rails.rb +3 -0
  55. data/rakelib/run_rspec.rake +0 -5
  56. data/rakelib/shakapacker_examples.rake +66 -23
  57. data/react_on_rails.gemspec +18 -8
  58. data/sig/react_on_rails/js_code_builder.rbs +11 -0
  59. data/sig/react_on_rails/render_request.rbs +28 -0
  60. data/sig/react_on_rails/rendering_strategy/exec_js_strategy.rbs +11 -0
  61. data/sig/react_on_rails/rendering_strategy.rbs +7 -0
  62. data/sig/react_on_rails.rbs +6 -0
  63. metadata +31 -10
@@ -7,6 +7,7 @@ require_relative "generator_helper"
7
7
  require_relative "generator_messages"
8
8
  require_relative "js_dependency_manager"
9
9
  require_relative "pro_setup"
10
+ require "react_on_rails/pro_migration"
10
11
 
11
12
  module ReactOnRails
12
13
  module Generators
@@ -103,43 +104,21 @@ module ReactOnRails
103
104
  end
104
105
 
105
106
  gemfile_content = File.read(gemfile_path)
106
- pro_gem_pattern = /^\s*gem(?:\s+|\(\s*(?:#.*\n\s*)*)["']react_on_rails_pro["']/
107
- base_gem_pattern = /^(\s*)gem(?:\s+|\(\s*)(["'])react_on_rails\2(?=\s*(?:,|\)|#|$))/
108
-
109
- has_pro_gem_entry = gemfile_content.match?(pro_gem_pattern)
107
+ has_pro_gem_entry = ReactOnRails::ProMigration.pro_gem_entry?(gemfile_content)
110
108
  had_pro_gem_entry_before_prerequisites =
111
- original_gemfile_content_for_rollback&.match?(pro_gem_pattern)
109
+ original_gemfile_content_for_rollback &&
110
+ ReactOnRails::ProMigration.pro_gem_entry?(original_gemfile_content_for_rollback)
112
111
  gemfile_lines = gemfile_content.lines
113
112
  updated_lines = []
114
- pro_entry_added = has_pro_gem_entry
115
113
  base_gem_entry_found = false
114
+ base_gem_entries_removed = false
116
115
  line_index = 0
117
116
 
118
117
  while line_index < gemfile_lines.length
119
118
  line = gemfile_lines[line_index]
120
- multiline_parenthesized_match = match_multiline_parenthesized_base_gem(gemfile_lines, line_index)
121
-
122
- if multiline_parenthesized_match
123
- base_gem_entry_found = true
124
- unless pro_entry_added
125
- indentation = multiline_parenthesized_match[:indentation]
126
- quote = multiline_parenthesized_match[:quote]
127
- updated_lines << build_pro_gem_replacement_line(
128
- indentation: indentation,
129
- quote: quote,
130
- suffix: multiline_parenthesized_match[:trailing_suffix],
131
- parenthesized_gem_call: true
132
- )
133
- pro_entry_added = true
134
- end
119
+ base_gem_declaration = ReactOnRails::ProMigration.base_gem_declaration_at(gemfile_lines, line_index)
135
120
 
136
- line_index = multiline_parenthesized_match[:next_index]
137
- next
138
- end
139
-
140
- match = line.match(base_gem_pattern)
141
-
142
- unless match
121
+ unless base_gem_declaration
143
122
  updated_lines << line
144
123
  line_index += 1
145
124
  next
@@ -147,25 +126,18 @@ module ReactOnRails
147
126
 
148
127
  base_gem_entry_found = true
149
128
 
150
- declaration = consume_non_parenthesized_base_gem_declaration(
151
- gemfile_lines,
152
- line_index,
153
- match.end(0)
154
- )
155
-
156
- unless pro_entry_added
157
- indentation = match[1]
158
- quote = match[2]
129
+ if has_pro_gem_entry
130
+ base_gem_entries_removed = true
131
+ else
159
132
  updated_lines << build_pro_gem_replacement_line(
160
- indentation: indentation,
161
- quote: quote,
162
- suffix: declaration[:trailing_suffix],
163
- parenthesized_gem_call: match[0].include?("(")
133
+ indentation: base_gem_declaration[:indentation],
134
+ quote: base_gem_declaration[:quote],
135
+ suffix: base_gem_declaration[:trailing_suffix],
136
+ parenthesized_gem_call: base_gem_declaration[:parenthesized_gem_call]
164
137
  )
165
- pro_entry_added = true
166
138
  end
167
139
 
168
- line_index = declaration[:next_index]
140
+ line_index = base_gem_declaration[:next_index]
169
141
  end
170
142
 
171
143
  updated_content = updated_lines.join
@@ -183,10 +155,6 @@ module ReactOnRails
183
155
  return true
184
156
  end
185
157
 
186
- if has_pro_gem_entry
187
- say "ℹ️ Existing react_on_rails_pro Gemfile entry detected; preserving current version constraint", :yellow
188
- end
189
-
190
158
  if options[:pretend]
191
159
  say_status :pretend, "Would replace react_on_rails with react_on_rails_pro in Gemfile", :yellow
192
160
  return true
@@ -194,7 +162,15 @@ module ReactOnRails
194
162
 
195
163
  original_gemfile_content = original_gemfile_content_for_rollback || gemfile_content
196
164
  atomic_write_file(gemfile_path, updated_content)
197
- say "✅ Replaced react_on_rails with react_on_rails_pro in Gemfile", :green
165
+ if base_gem_entries_removed
166
+ say(
167
+ "ℹ️ Existing react_on_rails_pro Gemfile entry detected; " \
168
+ "removed the now-stale react_on_rails entries",
169
+ :yellow
170
+ )
171
+ else
172
+ say "✅ Replaced react_on_rails with react_on_rails_pro in Gemfile", :green
173
+ end
198
174
  bundle_install_after_gem_swap(
199
175
  gemfile_path: gemfile_path,
200
176
  original_gemfile_content: original_gemfile_content
@@ -329,8 +305,8 @@ module ReactOnRails
329
305
  end
330
306
 
331
307
  def js_files_for_import_update
332
- js_extensions = %w[js jsx ts tsx mjs cjs vue svelte].join(",")
333
- %w[app/javascript app/frontend frontend javascript client].flat_map do |root|
308
+ js_extensions = ReactOnRails::ProMigration::JS_SOURCE_EXTENSIONS.join(",")
309
+ ReactOnRails::ProMigration::JS_SOURCE_ROOTS.flat_map do |root|
334
310
  root_path = File.join(destination_root, root)
335
311
  next [] unless Dir.exist?(root_path)
336
312
 
@@ -339,66 +315,84 @@ module ReactOnRails
339
315
  end.uniq
340
316
  end
341
317
 
342
- def rewrite_react_on_rails_module_specifiers(content)
343
- static_import_specifier_pattern = %r{
344
- (?<prefix>
345
- \A\s*(?:/\*.*?\*/\s*)?(?:import|export)(?:\s+type)?\s+.*?\s+from\s+|
346
- \A\s*[\w\}\],\*\$\s]+\s+from\s+
347
- )
348
- (?<quote>["'])
349
- react-on-rails(?!-pro)
350
- (?=(?:["']|/))
351
- }x
352
-
353
- dynamic_or_require_specifier_pattern = %r{
354
- (?<prefix>
355
- (?<!["'`])\bimport\s*\(\s*(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/\s*)*|
356
- (?<!["'`])\brequire\s*\(\s*(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/\s*)*
318
+ STATIC_IMPORT_SPECIFIER_PATTERN = %r{
319
+ (?<prefix>
320
+ \A\s*(?:/\*.*?\*/\s*)?(?:import|export)(?:\s+type)?\s+.*?\s+from\s+|
321
+ \A\s*[\w\}\],\*\$\s]+\s+from\s+
322
+ )
323
+ (?<quote>["'])
324
+ react-on-rails(?!-pro)
325
+ (?=(?:["']|/))
326
+ }x
327
+
328
+ DYNAMIC_OR_REQUIRE_SPECIFIER_PATTERN = %r{
329
+ (?<prefix>
330
+ (?<!["'`])\bimport\s*\(\s*(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/\s*)*|
331
+ (?<!["'`])\brequire\s*\(\s*(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/\s*)*
332
+ )
333
+ (?<quote>["'])
334
+ react-on-rails(?!-pro)
335
+ (?=(?:["']|/))
336
+ }x
337
+
338
+ SIDE_EFFECT_IMPORT_PATTERN = %r{
339
+ \A(?<prefix>\s*(?:/\*.*?\*/\s*)*import\s+)
340
+ (?<quote>["'])
341
+ react-on-rails(?!-pro)
342
+ (?=(?:["']|/))
343
+ }x
344
+
345
+ # Explicit allowlist of documented Jest/Vitest APIs whose first argument is a module specifier.
346
+ # Keep destructive rewrites narrow; the doctor can warn more broadly if needed.
347
+ JEST_MODULE_SPECIFIER_METHOD_PATTERN = ReactOnRails::ProMigration::JEST_MODULE_SPECIFIER_METHOD_PATTERN
348
+ VITEST_MODULE_SPECIFIER_METHOD_PATTERN = ReactOnRails::ProMigration::VITEST_MODULE_SPECIFIER_METHOD_PATTERN
349
+
350
+ MOCK_CALL_PATTERN = %r{
351
+ (?<prefix>
352
+ (?<!["'`])\b(?:
353
+ jest\.(?:#{JEST_MODULE_SPECIFIER_METHOD_PATTERN})
354
+ |
355
+ vi\.(?:#{VITEST_MODULE_SPECIFIER_METHOD_PATTERN})
357
356
  )
358
- (?<quote>["'])
359
- react-on-rails(?!-pro)
360
- (?=(?:["']|/))
361
- }x
362
-
363
- side_effect_import_pattern = %r{
364
- \A(?<prefix>\s*(?:/\*.*?\*/\s*)*import\s+)
365
- (?<quote>["'])
366
- react-on-rails(?!-pro)
367
- (?=(?:["']|/))
368
- }x
357
+ \s*
358
+ (?:<[^;\n]*>\s*)?
359
+ \s*\(\s*
360
+ )
361
+ (?<quote>["'])
362
+ react-on-rails(?!-pro)
363
+ (?=(?:["']|/))
364
+ }x
365
+
366
+ DECLARE_MODULE_PATTERN = %r{
367
+ \A(?<prefix>\s*(?:export\s+)?declare\s+module\s+)
368
+ (?<quote>["'])
369
+ react-on-rails(?!-pro)
370
+ (?=(?:["']|/))
371
+ }x
372
+
373
+ BASE_PACKAGE_REWRITE_PATTERNS = [
374
+ STATIC_IMPORT_SPECIFIER_PATTERN,
375
+ DYNAMIC_OR_REQUIRE_SPECIFIER_PATTERN,
376
+ SIDE_EFFECT_IMPORT_PATTERN,
377
+ MOCK_CALL_PATTERN,
378
+ DECLARE_MODULE_PATTERN
379
+ ].freeze
380
+ GEMFILE_STRING_DELIMITERS = ["'", '"', "`"].freeze
369
381
 
382
+ def rewrite_react_on_rails_module_specifiers(content)
370
383
  rewrite_non_comment_lines(content) do |line|
371
384
  rewrite_outside_inline_template_literals(line) do |line_without_templates|
372
- rewritten_line = line_without_templates.gsub(static_import_specifier_pattern) do
373
- "#{Regexp.last_match[:prefix]}#{Regexp.last_match[:quote]}react-on-rails-pro"
374
- end
375
-
376
- rewritten_line = rewritten_line.gsub(dynamic_or_require_specifier_pattern) do
377
- "#{Regexp.last_match[:prefix]}#{Regexp.last_match[:quote]}react-on-rails-pro"
378
- end
379
-
380
- rewritten_line.gsub(side_effect_import_pattern) do
381
- "#{Regexp.last_match[:prefix]}#{Regexp.last_match[:quote]}react-on-rails-pro"
382
- end
385
+ rewrite_base_package_patterns(line_without_templates)
383
386
  end
384
387
  end
385
388
  end
386
389
 
387
- def line_continues_with_comma?(line)
388
- line_without_comment = line.sub(/\s*#.*$/, "").rstrip
389
- line_without_comment.end_with?(",")
390
- end
391
-
392
- def gem_declaration_continues_on_next_line?(line)
393
- stripped = line.lstrip
394
- return true if stripped.empty?
395
-
396
- !stripped.match?(/\Agem(?:\s|\()/)
397
- end
398
-
399
- def comment_or_blank_line?(line)
400
- stripped = line.lstrip
401
- stripped.empty? || stripped.start_with?("#")
390
+ def rewrite_base_package_patterns(line)
391
+ BASE_PACKAGE_REWRITE_PATTERNS.reduce(line) do |result, pattern|
392
+ result.gsub(pattern) do
393
+ "#{Regexp.last_match[:prefix]}#{Regexp.last_match[:quote]}react-on-rails-pro"
394
+ end
395
+ end
402
396
  end
403
397
 
404
398
  def add_missing_gemfile_warning(gemfile_path)
@@ -562,23 +556,72 @@ module ReactOnRails
562
556
  comment_balance.positive?
563
557
  end
564
558
 
559
+ MODULE_SPECIFIER_CALL_START_PATTERN = /
560
+ (?<![\w$])(?:import|require)\s*\(
561
+ |
562
+ (?<!["'`])\b(?:
563
+ jest\.(?:#{JEST_MODULE_SPECIFIER_METHOD_PATTERN})
564
+ |
565
+ vi\.(?:#{VITEST_MODULE_SPECIFIER_METHOD_PATTERN})
566
+ )
567
+ \s*
568
+ (?:<[^;\n]*>\s*)?
569
+ \s*\(
570
+ /x
571
+
572
+ MODULE_SPECIFIER_CALL_WITH_STRING_PATTERN = %r{
573
+ (?<!["'`])\b(?:import|require)\s*\(\s*(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/\s*)*["']
574
+ |
575
+ (?<!["'`])\b(?:
576
+ jest\.(?:#{JEST_MODULE_SPECIFIER_METHOD_PATTERN})
577
+ |
578
+ vi\.(?:#{VITEST_MODULE_SPECIFIER_METHOD_PATTERN})
579
+ )
580
+ \s*
581
+ (?:<[^;\n]*>\s*)?
582
+ \s*\(\s*
583
+ (?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/\s*)*
584
+ ["']
585
+ }x
586
+
565
587
  def starts_pending_multiline_module_call?(line)
566
588
  line_without_literals = line_without_string_literals_and_inline_comments(line)
567
- return false unless line_without_literals.match?(/(?<![\w$])(?:import|require)\s*\(/)
589
+ return false unless line_without_literals.match?(MODULE_SPECIFIER_CALL_START_PATTERN)
568
590
 
569
- !line.match?(%r{(?<!["'`])\b(?:import|require)\s*\(\s*(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/\s*)*["']})
591
+ !line.match?(MODULE_SPECIFIER_CALL_WITH_STRING_PATTERN)
570
592
  end
571
593
 
594
+ PENDING_MODULE_SPECIFIER_PATTERN = %r{(?<quote>["'])react-on-rails(?!-pro)(?=(?:["']|/))}
595
+
572
596
  def rewrite_pending_module_specifier(line)
573
- line.gsub(%r{(?<quote>["'])react-on-rails(?!-pro)(?=(?:["']|/))}) do
597
+ match = line.match(PENDING_MODULE_SPECIFIER_PATTERN)
598
+ return line unless match
599
+
600
+ rewritten_line = line.sub(PENDING_MODULE_SPECIFIER_PATTERN) do
574
601
  "#{Regexp.last_match[:quote]}react-on-rails-pro"
575
602
  end
603
+
604
+ rewrite_statement_suffix_after_pending_module_specifier(rewritten_line, match)
605
+ end
606
+
607
+ def rewrite_statement_suffix_after_pending_module_specifier(line, pending_match)
608
+ closing_quote_index = line.index(pending_match[:quote], pending_match.end(0))
609
+ return line unless closing_quote_index
610
+
611
+ suffix = line[(closing_quote_index + 1)..].to_s
612
+ separator_match = suffix.match(/\A(?<separator>\s*;\s*)/)
613
+ return line unless separator_match
614
+
615
+ suffix_code = suffix[separator_match.end(0)..].to_s
616
+ rewritten_suffix_code = rewrite_base_package_patterns(suffix_code)
617
+ "#{line[0..closing_quote_index]}#{separator_match[:separator]}#{rewritten_suffix_code}"
576
618
  end
577
619
 
578
620
  def update_pending_multiline_module_call_tracking(line, pending_depth)
579
621
  if pending_depth.positive?
580
622
  rewritten_line = rewrite_pending_module_specifier(line)
581
623
  updated_depth = pending_depth + module_call_parenthesis_delta(rewritten_line)
624
+ updated_depth = 0 if rewritten_line != line
582
625
  updated_depth = 0 if updated_depth <= 0
583
626
  [rewritten_line, updated_depth]
584
627
  elsif starts_pending_multiline_module_call?(line)
@@ -617,7 +660,8 @@ module ReactOnRails
617
660
  def module_call_parenthesis_delta(line, from_module_call_start: false)
618
661
  line_without_literals = line_without_string_literals_and_inline_comments(line)
619
662
  line_to_measure = if from_module_call_start
620
- line_without_literals.sub(/\A.*?(?<![\w$])(?:import|require)\s*\(/, "(")
663
+ match = line_without_literals.match(MODULE_SPECIFIER_CALL_START_PATTERN)
664
+ match ? "(#{line_without_literals[match.end(0)..]}" : line_without_literals
621
665
  else
622
666
  line_without_literals
623
667
  end
@@ -823,96 +867,75 @@ module ReactOnRails
823
867
  nil
824
868
  end
825
869
 
826
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
827
- def match_multiline_parenthesized_base_gem(lines, start_index)
828
- start_line = lines[start_index]
829
- start_match = start_line.match(/^(\s*)gem\s*\(/)
830
- return nil unless start_match
831
-
832
- line_index = start_index
833
- found_base_gem_name = false
834
- base_gem_quote = nil
835
- gem_name_line_index = nil
836
- gem_name_match_end = nil
837
- paren_depth = 0
838
-
839
- while line_index < lines.length
840
- line = lines[line_index]
841
- line_without_comment = line.sub(/\s*#.*$/, "")
842
- line_without_literals = line_without_string_literals_and_inline_comments(line, strip_ruby_comments: true)
843
-
844
- if !found_base_gem_name &&
845
- (gem_name_match = line_without_comment.match(/(["'])react_on_rails\1(?=\s*(?:,|\)|#|$))/))
846
- found_base_gem_name = true
847
- base_gem_quote = gem_name_match[1]
848
- gem_name_line_index = line_index
849
- gem_name_match_end = gem_name_match.end(0)
850
- end
870
+ def build_pro_gem_replacement_line(indentation:, quote:, suffix:, parenthesized_gem_call: false)
871
+ normalized_suffix = suffix || "\n"
872
+ normalized_suffix = "#{normalized_suffix}\n" unless normalized_suffix.end_with?("\n")
851
873
 
852
- paren_depth += line_without_literals.count("(") - line_without_literals.count(")")
874
+ has_user_version_pin = normalized_suffix.match?(/\A\s*,\s*(?:#[^\n]*\n\s*)*["']/)
875
+ version_arg = has_user_version_pin ? "" : ", #{quote}#{pro_gem_version_requirement}#{quote}"
853
876
 
854
- if paren_depth <= 0
855
- return nil unless found_base_gem_name
877
+ if parenthesized_gem_call
878
+ normalized_suffix = remove_parenthesized_gem_call_closing_parenthesis(normalized_suffix)
879
+ end
880
+ normalized_suffix = "\n" if normalized_suffix.match?(/\A,\s*\n\z/)
856
881
 
857
- declaration_fragment = lines[gem_name_line_index..line_index].join
858
- suffix = declaration_fragment[gem_name_match_end..]
859
- suffix = "\n" if suffix.nil? || suffix.empty?
860
- return {
861
- indentation: start_match[1],
862
- quote: base_gem_quote,
863
- next_index: line_index + 1,
864
- trailing_suffix: suffix
865
- }
866
- end
882
+ "#{indentation}gem #{quote}react_on_rails_pro#{quote}#{version_arg}#{normalized_suffix}"
883
+ end
867
884
 
868
- line_index += 1
869
- end
885
+ def remove_parenthesized_gem_call_closing_parenthesis(suffix)
886
+ closing_index = parenthesized_gem_call_closing_parenthesis_index(suffix)
887
+ return suffix unless closing_index
870
888
 
871
- nil
889
+ prefix = suffix[0...closing_index]
890
+ rest = suffix[(closing_index + 1)..].to_s
891
+ return "#{prefix.rstrip} #{rest.lstrip}" if closing_parenthesis_line_has_postfix_code?(rest)
892
+
893
+ "#{prefix.chomp}#{rest}"
872
894
  end
873
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
874
-
875
- def consume_non_parenthesized_base_gem_declaration(lines, start_index, match_end)
876
- line_index = start_index
877
- current_line = lines[line_index]
878
- declaration_lines = [current_line]
879
- line_index += 1
880
-
881
- while line_index < lines.length &&
882
- line_continues_with_comma?(current_line) &&
883
- gem_declaration_continues_on_next_line?(lines[line_index])
884
- next_line = lines[line_index]
885
- declaration_lines << next_line
886
- current_line = next_line unless comment_or_blank_line?(next_line)
887
- line_index += 1
888
- end
889
895
 
890
- trailing_suffix = lines[start_index][match_end..].to_s + declaration_lines.drop(1).join
891
- { trailing_suffix: trailing_suffix, next_index: line_index }
896
+ def closing_parenthesis_line_has_postfix_code?(rest)
897
+ stripped_rest = rest.lstrip
898
+ !stripped_rest.empty? && !stripped_rest.start_with?("#", "\n", "\r")
892
899
  end
893
900
 
894
- def build_pro_gem_replacement_line(indentation:, quote:, suffix:, parenthesized_gem_call: false)
895
- normalized_suffix = suffix || "\n"
896
- normalized_suffix = "#{normalized_suffix}\n" unless normalized_suffix.end_with?("\n")
897
- version_arg_pattern = /\A(?<prefix>\s*,(?:\s*#.*\n|\s++)*)["'][^"']*["'](?<trailing_comma>\s*,)?/
898
- loop do
899
- updated_suffix = normalized_suffix.sub(version_arg_pattern) do
900
- if Regexp.last_match[:trailing_comma]
901
- Regexp.last_match[:prefix].sub(/\n[ \t]*\z/, "")
902
- else
903
- ""
904
- end
901
+ # The suffix starts after the gem name but still inside the original `gem(...)` call,
902
+ # so the matching call-closing parenthesis is found by starting at depth 1.
903
+ def parenthesized_gem_call_closing_parenthesis_index(suffix)
904
+ depth = 1
905
+ quote = nil
906
+ scan_index = 0
907
+
908
+ while scan_index < suffix.length
909
+ char = suffix[scan_index]
910
+
911
+ if quote
912
+ quote = nil if char == quote && !character_escaped?(suffix, scan_index)
913
+ else
914
+ scan_index, depth, quote, closing_index =
915
+ next_parenthesized_gem_suffix_scan_state(suffix, scan_index, depth)
916
+ return closing_index if closing_index
917
+ return nil unless scan_index
905
918
  end
906
- break if updated_suffix == normalized_suffix
907
919
 
908
- normalized_suffix = updated_suffix
920
+ scan_index += 1
909
921
  end
910
- normalized_suffix = normalized_suffix.sub(/\A,\s*(?:#[^\n]*)?\n\z/, "\n")
911
- normalized_suffix = normalized_suffix.sub(/\A,[ \t]{2,}/, ", ")
912
- normalized_suffix = normalized_suffix.sub(/\)(\s*(?:#.*)?\n)\z/, '\1') if parenthesized_gem_call
913
922
 
914
- "#{indentation}gem #{quote}react_on_rails_pro#{quote}, " \
915
- "#{quote}#{pro_gem_version_requirement}#{quote}#{normalized_suffix}"
923
+ nil
924
+ end
925
+
926
+ def next_parenthesized_gem_suffix_scan_state(suffix, scan_index, depth)
927
+ char = suffix[scan_index]
928
+ return [scan_index, depth, char, nil] if GEMFILE_STRING_DELIMITERS.include?(char)
929
+ return [suffix.index("\n", scan_index), depth, nil, nil] if char == "#"
930
+ return [scan_index, depth + 1, nil, nil] if char == "("
931
+ return parenthesized_gem_suffix_closing_state(scan_index, depth) if char == ")"
932
+
933
+ [scan_index, depth, nil, nil]
934
+ end
935
+
936
+ def parenthesized_gem_suffix_closing_state(scan_index, depth)
937
+ next_depth = depth - 1
938
+ [scan_index, next_depth, nil, next_depth.zero? ? scan_index : nil]
916
939
  end
917
940
 
918
941
  def print_success_message