react_on_rails 17.1.0 → 17.2.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.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/Gemfile.development_dependencies +2 -2
- data/Gemfile.lock +65 -73
- data/Steepfile +1 -0
- data/docs/agent/install-and-upgrade.md +8 -3
- data/docs/agent/rsc-adoption.md +3 -0
- data/lib/generators/react_on_rails/base_generator.rb +2 -1
- data/lib/generators/react_on_rails/generated_webpack_config_pair.rb +84 -0
- data/lib/generators/react_on_rails/generator_helper.rb +2 -4
- data/lib/generators/react_on_rails/generator_messages.rb +1 -1
- data/lib/generators/react_on_rails/install_generator.rb +2 -1
- data/lib/generators/react_on_rails/js_dependency_manager.rb +14 -14
- data/lib/generators/react_on_rails/pro/USAGE +14 -3
- data/lib/generators/react_on_rails/pro_generator.rb +1 -1
- data/lib/generators/react_on_rails/pro_setup.rb +41 -213
- data/lib/generators/react_on_rails/rsc_setup.rb +7 -16
- data/lib/generators/react_on_rails/templates/pro/base/config/initializers/react_on_rails_pro.rb.tt +1 -1
- data/lib/react_on_rails/agent_guardrails.rb +217 -39
- data/lib/react_on_rails/dev/process_manager.rb +14 -0
- data/lib/react_on_rails/dev/server_manager.rb +1727 -103
- data/lib/react_on_rails/doctor.rb +875 -44
- data/lib/react_on_rails/helper.rb +3 -3
- data/lib/react_on_rails/react_component/render_options.rb +5 -7
- data/lib/react_on_rails/server_rendering_pool/ruby_embedded_java_script.rb +27 -116
- data/lib/react_on_rails/utils.rb +65 -0
- data/lib/react_on_rails/version.rb +1 -1
- data/lib/react_on_rails/version_checker/lockfile_resolution.rb +170 -0
- data/lib/react_on_rails/version_checker.rb +43 -22
- data/rakelib/run_rspec.rake +6 -1
- data/sig/react_on_rails/configuration.rbs +28 -26
- data/sig/react_on_rails/controller.rbs +2 -1
- data/sig/react_on_rails/dev/process_manager.rbs +1 -0
- data/sig/react_on_rails/dev/server_manager.rbs +150 -10
- data/sig/react_on_rails/git_utils.rbs +5 -1
- data/sig/react_on_rails/locales.rbs +4 -4
- data/sig/react_on_rails/prerender_error.rbs +2 -2
- data/sig/react_on_rails/test_helper.rbs +15 -1
- data/sig/react_on_rails/utils.rbs +2 -2
- data/sig/react_on_rails/version_checker/lockfile_resolution.rbs +31 -0
- data/sig/react_on_rails/version_checker.rbs +30 -7
- metadata +4 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "erb"
|
|
5
5
|
require "open3"
|
|
6
|
+
require "ripper"
|
|
6
7
|
require "stringio"
|
|
7
8
|
require "tempfile"
|
|
8
9
|
require "timeout"
|
|
@@ -71,6 +72,16 @@ module ReactOnRails
|
|
|
71
72
|
SERVER_BUNDLE_SOURCE_EXTENSIONS = %w[.js .jsx .ts .tsx .mjs .cjs].freeze
|
|
72
73
|
CUSTOM_LAUNCHER_INDICATOR_FILES = %w[dev].freeze
|
|
73
74
|
RAILS_SERVER_COMMAND_REGEX = %r{\b(?:(?:bin/)?rails\s+(?:server|s)|puma|unicorn|rackup|passenger\s+start)\b}
|
|
75
|
+
TEMPLATE_HTML_COMMENT_PATTERN = /<!--.*?-->/m
|
|
76
|
+
TEMPLATE_COMMENT_PATTERN = /<!--.*?-->|<%#.*?%>/m
|
|
77
|
+
ERB_EXPRESSION_PATTERN = /<%(?![#%])={0,2}\s*(?<expression>.*?)-?%>/m
|
|
78
|
+
ERB_OUTPUT_EXPRESSION_PATTERN = /<%={1,2}\s*(?<expression>.*?)-?%>/m
|
|
79
|
+
ERB_NON_OUTPUT_EXPRESSION_PATTERN = /<%(?![#%=])\s*(?<expression>.*?)-?%>/m
|
|
80
|
+
ERB_CONTROL_FLOW_KEYWORDS = %w[
|
|
81
|
+
begin case do else elsif end ensure for if rescue return then unless until when while
|
|
82
|
+
].freeze
|
|
83
|
+
REFLECTIVE_HELPER_DISPATCH_METHODS = %w[send public_send __send__ method public_method].freeze
|
|
84
|
+
REACT_ON_RAILS_INITIALIZER_PATH = "config/initializers/react_on_rails.rb"
|
|
74
85
|
|
|
75
86
|
# Deprecated-renderer-cache scan (used by check_deprecated_renderer_cache_task):
|
|
76
87
|
# look for references to the old pre_stage_bundle_for_node_renderer task in
|
|
@@ -518,6 +529,7 @@ module ReactOnRails
|
|
|
518
529
|
check_shakapacker_configuration_details
|
|
519
530
|
check_react_on_rails_configuration_details
|
|
520
531
|
check_server_bundle_prerender_consistency
|
|
532
|
+
check_layout_files
|
|
521
533
|
end
|
|
522
534
|
|
|
523
535
|
def check_bin_dev_launcher
|
|
@@ -1231,7 +1243,6 @@ module ReactOnRails
|
|
|
1231
1243
|
)
|
|
1232
1244
|
end
|
|
1233
1245
|
|
|
1234
|
-
check_layout_files
|
|
1235
1246
|
check_server_rendering_engine
|
|
1236
1247
|
end
|
|
1237
1248
|
|
|
@@ -1242,13 +1253,15 @@ module ReactOnRails
|
|
|
1242
1253
|
checker.add_info("\n📄 Layout Files Analysis:")
|
|
1243
1254
|
|
|
1244
1255
|
layout_files.each do |layout_file|
|
|
1245
|
-
next unless
|
|
1256
|
+
next unless analyzable_layout_file?(layout_file)
|
|
1246
1257
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1258
|
+
template_content = File.read(layout_file)
|
|
1259
|
+
ambiguous_commented_erb = html_comment_with_executable_erb?(template_content)
|
|
1260
|
+
content = uncommented_template_content(template_content)
|
|
1261
|
+
has_stylesheet = stylesheet_pack_helper_evidence?(content)
|
|
1262
|
+
has_javascript = erb_output_helper?(content, "javascript_pack_tag")
|
|
1250
1263
|
|
|
1251
|
-
layout_name =
|
|
1264
|
+
layout_name = layout_file.delete_prefix("app/views/layouts/").delete_suffix(".html.erb")
|
|
1252
1265
|
|
|
1253
1266
|
# Report detected pack helpers informationally. A JavaScript-only entrypoint
|
|
1254
1267
|
# (no emitted CSS), a CSS-only pack, or a hybrid layout that loads CSS via
|
|
@@ -1260,12 +1273,764 @@ module ReactOnRails
|
|
|
1260
1273
|
checker.add_info(" ℹ️ #{layout_name}: has stylesheet_pack_tag")
|
|
1261
1274
|
elsif has_javascript
|
|
1262
1275
|
checker.add_info(" ℹ️ #{layout_name}: has javascript_pack_tag")
|
|
1276
|
+
check_unflushed_auto_loaded_component_css(layout_name, content, ambiguous_commented_erb:)
|
|
1263
1277
|
else
|
|
1264
1278
|
checker.add_info(" ℹ️ #{layout_name}: no pack tags found")
|
|
1265
1279
|
end
|
|
1266
1280
|
end
|
|
1267
1281
|
end
|
|
1268
1282
|
|
|
1283
|
+
def analyzable_layout_file?(layout_file)
|
|
1284
|
+
!File.basename(layout_file).start_with?("_") && File.exist?(layout_file)
|
|
1285
|
+
end
|
|
1286
|
+
|
|
1287
|
+
def check_unflushed_auto_loaded_component_css(layout_name, content, ambiguous_commented_erb: false)
|
|
1288
|
+
return if ambiguous_commented_erb
|
|
1289
|
+
return if separate_erb_control_flow?(content)
|
|
1290
|
+
return unless erb_output_helper?(content, "javascript_pack_tag")
|
|
1291
|
+
return if erb_helper_mentioned?(content, "render")
|
|
1292
|
+
|
|
1293
|
+
component_names = auto_loaded_component_names(content)
|
|
1294
|
+
component_names.concat(auto_loaded_component_names_from_implicit_views(layout_name)).uniq.each do |component_name|
|
|
1295
|
+
entrypoint = "generated/#{component_name}"
|
|
1296
|
+
stylesheet_assets = static_stylesheet_assets(entrypoint)
|
|
1297
|
+
next unless stylesheet_assets
|
|
1298
|
+
|
|
1299
|
+
checker.add_warning(
|
|
1300
|
+
"⚠️ #{layout_name}: auto-loaded component entrypoint #{entrypoint} emits CSS " \
|
|
1301
|
+
"(#{stylesheet_assets.join(', ')}), but this React layout flushes javascript_pack_tag without " \
|
|
1302
|
+
"stylesheet_pack_tag"
|
|
1303
|
+
)
|
|
1304
|
+
end
|
|
1305
|
+
end
|
|
1306
|
+
|
|
1307
|
+
def stylesheet_pack_helper_evidence?(content)
|
|
1308
|
+
erb_output_helper?(content, "stylesheet_pack_tag") ||
|
|
1309
|
+
stylesheet_pack_ambiguity_evidence?(content)
|
|
1310
|
+
end
|
|
1311
|
+
|
|
1312
|
+
def stylesheet_pack_ambiguity_evidence?(content)
|
|
1313
|
+
ignored_bare_local = "stylesheet_pack_tag" if erb_local_binding?(content, "stylesheet_pack_tag")
|
|
1314
|
+
stylesheet_pack_output_ambiguity_evidence?(content, ignored_bare_local:) ||
|
|
1315
|
+
content.scan(ERB_NON_OUTPUT_EXPRESSION_PATTERN).any? do |(expression)|
|
|
1316
|
+
statements = ripper_statements(expression)
|
|
1317
|
+
next false if statements.one? &&
|
|
1318
|
+
discarded_top_level_helper_call?(statements.first, "stylesheet_pack_tag")
|
|
1319
|
+
|
|
1320
|
+
statements.any? do |statement|
|
|
1321
|
+
helper_call_context_evidence?(statement, "stylesheet_pack_tag", ignored_bare_local:)
|
|
1322
|
+
end
|
|
1323
|
+
end
|
|
1324
|
+
end
|
|
1325
|
+
|
|
1326
|
+
def stylesheet_pack_output_ambiguity_evidence?(content, ignored_bare_local: nil)
|
|
1327
|
+
content.scan(ERB_OUTPUT_EXPRESSION_PATTERN).any? do |(expression)|
|
|
1328
|
+
statements = ripper_statements(expression)
|
|
1329
|
+
statements.any? do |statement|
|
|
1330
|
+
helper_call_context_evidence?(statement, "stylesheet_pack_tag", ignored_bare_local:)
|
|
1331
|
+
end
|
|
1332
|
+
end
|
|
1333
|
+
end
|
|
1334
|
+
|
|
1335
|
+
def static_stylesheet_assets(entrypoint)
|
|
1336
|
+
assets = static_manifest_assets(entrypoint)
|
|
1337
|
+
stylesheet_assets = assets["css"] if assets.is_a?(Hash)
|
|
1338
|
+
return unless stylesheet_assets.is_a?(Array) && stylesheet_assets.any?
|
|
1339
|
+
return unless homogeneous_manifest_css_schema?(stylesheet_assets)
|
|
1340
|
+
|
|
1341
|
+
normalized_assets = stylesheet_assets.map { |asset| static_manifest_asset_path(asset) }
|
|
1342
|
+
return if normalized_assets.any?(&:nil?)
|
|
1343
|
+
|
|
1344
|
+
normalized_assets
|
|
1345
|
+
end
|
|
1346
|
+
|
|
1347
|
+
def homogeneous_manifest_css_schema?(assets)
|
|
1348
|
+
assets.all?(String) || assets.all?(Hash)
|
|
1349
|
+
end
|
|
1350
|
+
|
|
1351
|
+
def static_manifest_asset_path(asset)
|
|
1352
|
+
path = asset.is_a?(Hash) ? asset["src"] : asset
|
|
1353
|
+
path if path.is_a?(String) && !path.empty?
|
|
1354
|
+
end
|
|
1355
|
+
|
|
1356
|
+
def static_manifest_assets(entrypoint)
|
|
1357
|
+
manifest = shakapacker_manifest_data
|
|
1358
|
+
return unless manifest.is_a?(Hash)
|
|
1359
|
+
|
|
1360
|
+
entrypoints = manifest["entrypoints"]
|
|
1361
|
+
return unless entrypoints.is_a?(Hash)
|
|
1362
|
+
|
|
1363
|
+
entrypoint_data = entrypoints[entrypoint]
|
|
1364
|
+
entrypoint_data["assets"] if entrypoint_data.is_a?(Hash)
|
|
1365
|
+
end
|
|
1366
|
+
|
|
1367
|
+
def auto_loaded_component_names_from_implicit_views(layout_name)
|
|
1368
|
+
Dir.glob("app/controllers/**/*_controller.rb").flat_map do |controller_file|
|
|
1369
|
+
implicit_view_paths_for_layout(controller_file, layout_name).flat_map do |view_path|
|
|
1370
|
+
auto_loaded_component_names_from_implicit_view(view_path)
|
|
1371
|
+
end
|
|
1372
|
+
rescue StandardError
|
|
1373
|
+
[]
|
|
1374
|
+
end
|
|
1375
|
+
rescue StandardError
|
|
1376
|
+
[]
|
|
1377
|
+
end
|
|
1378
|
+
|
|
1379
|
+
def auto_loaded_component_names_from_implicit_view(view_path)
|
|
1380
|
+
return [] unless File.exist?(view_path)
|
|
1381
|
+
|
|
1382
|
+
template_content = File.read(view_path)
|
|
1383
|
+
return [] if html_comment_with_executable_erb?(template_content)
|
|
1384
|
+
|
|
1385
|
+
view_content = uncommented_template_content(template_content)
|
|
1386
|
+
return [] if stylesheet_pack_helper_evidence?(view_content)
|
|
1387
|
+
return [] if erb_helper_mentioned?(view_content, "render")
|
|
1388
|
+
|
|
1389
|
+
auto_loaded_component_names(view_content)
|
|
1390
|
+
end
|
|
1391
|
+
|
|
1392
|
+
def implicit_view_paths_for_layout(controller_file, layout_name)
|
|
1393
|
+
action_names = statically_associated_action_names(controller_file, layout_name)
|
|
1394
|
+
return [] unless action_names
|
|
1395
|
+
|
|
1396
|
+
view_directory = controller_file.delete_prefix("app/controllers/").delete_suffix("_controller.rb")
|
|
1397
|
+
action_names.map do |action_name|
|
|
1398
|
+
"app/views/#{view_directory}/#{action_name}.html.erb"
|
|
1399
|
+
end
|
|
1400
|
+
rescue StandardError
|
|
1401
|
+
[]
|
|
1402
|
+
end
|
|
1403
|
+
|
|
1404
|
+
def statically_associated_action_names(controller_file, layout_name)
|
|
1405
|
+
return unless File.exist?(controller_file)
|
|
1406
|
+
|
|
1407
|
+
controller = static_controller_class(controller_file)
|
|
1408
|
+
statements = static_body_statements(controller[3]) if controller
|
|
1409
|
+
return unless statements
|
|
1410
|
+
|
|
1411
|
+
layouts = statements.filter_map { |statement| static_layout_declaration(statement) }
|
|
1412
|
+
action_names = statements.filter_map { |statement| static_generated_action_name(statement) }
|
|
1413
|
+
return unless exact_generated_controller_statements?(statements, layouts, action_names, layout_name)
|
|
1414
|
+
|
|
1415
|
+
action_names
|
|
1416
|
+
end
|
|
1417
|
+
|
|
1418
|
+
def exact_generated_controller_statements?(statements, layouts, action_names, layout_name)
|
|
1419
|
+
layouts == [layout_name] && action_names.one? &&
|
|
1420
|
+
statements.length == layouts.length + action_names.length
|
|
1421
|
+
end
|
|
1422
|
+
|
|
1423
|
+
def static_controller_class(controller_file)
|
|
1424
|
+
statements = ripper_statements(File.read(controller_file))
|
|
1425
|
+
return unless statements.one?
|
|
1426
|
+
|
|
1427
|
+
controller = statements.first
|
|
1428
|
+
return unless ast_node?(controller, :class)
|
|
1429
|
+
return unless static_constant_reference(controller[2]) == "ApplicationController"
|
|
1430
|
+
|
|
1431
|
+
relative_name = controller_file.delete_prefix("app/controllers/").delete_suffix("_controller.rb")
|
|
1432
|
+
expected_name = "#{relative_name.split('/').map(&:camelize).join('::')}Controller"
|
|
1433
|
+
controller if static_constant_name(controller[1]) == expected_name
|
|
1434
|
+
end
|
|
1435
|
+
|
|
1436
|
+
def static_constant_name(node)
|
|
1437
|
+
return unless node.is_a?(Array)
|
|
1438
|
+
|
|
1439
|
+
case node.first
|
|
1440
|
+
when :const_ref, :top_const_ref
|
|
1441
|
+
static_constant_token_value(node[1])
|
|
1442
|
+
when :const_path_ref
|
|
1443
|
+
namespace = static_constant_name(node[1])
|
|
1444
|
+
constant = static_constant_token_value(node[2])
|
|
1445
|
+
"#{namespace}::#{constant}" if namespace && constant
|
|
1446
|
+
end
|
|
1447
|
+
end
|
|
1448
|
+
|
|
1449
|
+
def static_constant_token_value(node)
|
|
1450
|
+
node[1] if ast_node?(node, :@const)
|
|
1451
|
+
end
|
|
1452
|
+
|
|
1453
|
+
def static_layout_declaration(node)
|
|
1454
|
+
arguments = static_argument_values(static_call_arguments(node, "layout"))
|
|
1455
|
+
return unless arguments&.one?
|
|
1456
|
+
|
|
1457
|
+
static_string_value(arguments.first)
|
|
1458
|
+
end
|
|
1459
|
+
|
|
1460
|
+
def static_generated_action_name(node)
|
|
1461
|
+
return unless ast_node?(node, :def) && token_type?(node[1], :@ident)
|
|
1462
|
+
return unless empty_method_parameters?(node[2])
|
|
1463
|
+
return unless static_generated_action_body?(node[3])
|
|
1464
|
+
|
|
1465
|
+
node[1][1]
|
|
1466
|
+
end
|
|
1467
|
+
|
|
1468
|
+
def static_generated_action_body?(node)
|
|
1469
|
+
return false unless ast_node?(node, :bodystmt) && node.drop(2).all?(&:nil?)
|
|
1470
|
+
|
|
1471
|
+
statements = static_body_statements(node)
|
|
1472
|
+
statements&.all? { |statement| ast_node?(statement, :void_stmt) || static_ivar_assignment?(statement) }
|
|
1473
|
+
end
|
|
1474
|
+
|
|
1475
|
+
def static_ivar_assignment?(node)
|
|
1476
|
+
return false unless ast_node?(node, :assign)
|
|
1477
|
+
|
|
1478
|
+
field = node[1]
|
|
1479
|
+
ast_node?(field, :var_field) && token_type?(field[1], :@ivar) && static_data_value?(node[2])
|
|
1480
|
+
end
|
|
1481
|
+
|
|
1482
|
+
def empty_method_parameters?(node)
|
|
1483
|
+
ast_node?(node, :params) && node.drop(1).all?(&:nil?)
|
|
1484
|
+
end
|
|
1485
|
+
|
|
1486
|
+
def static_body_statements(node)
|
|
1487
|
+
statements = node[1] if ast_node?(node, :bodystmt)
|
|
1488
|
+
statements if statements.is_a?(Array)
|
|
1489
|
+
end
|
|
1490
|
+
|
|
1491
|
+
def erb_output_helper?(content, helper_name)
|
|
1492
|
+
unqualified_call_ambiguous = erb_local_binding?(content, helper_name)
|
|
1493
|
+
|
|
1494
|
+
content.scan(ERB_OUTPUT_EXPRESSION_PATTERN).any? do |(expression)|
|
|
1495
|
+
statements = ripper_statements(expression)
|
|
1496
|
+
statements.one? && emitted_top_level_helper_call?(
|
|
1497
|
+
statements.first,
|
|
1498
|
+
helper_name,
|
|
1499
|
+
unqualified_call_ambiguous:
|
|
1500
|
+
)
|
|
1501
|
+
end
|
|
1502
|
+
end
|
|
1503
|
+
|
|
1504
|
+
def emitted_top_level_helper_call?(node, helper_name, unqualified_call_ambiguous: false)
|
|
1505
|
+
return true if direct_self_helper_call?(node, helper_name)
|
|
1506
|
+
return true if parenthesized_self_javascript_helper_call?(node, helper_name)
|
|
1507
|
+
return false if unqualified_call_ambiguous && bare_unqualified_helper_vcall?(node, helper_name)
|
|
1508
|
+
|
|
1509
|
+
direct_unqualified_helper_call?(node, helper_name)
|
|
1510
|
+
end
|
|
1511
|
+
|
|
1512
|
+
def erb_local_binding?(content, local_name)
|
|
1513
|
+
content.scan(ERB_EXPRESSION_PATTERN).any? do |(expression)|
|
|
1514
|
+
ripper_statements(expression).any? { |statement| source_proven_local_binding?(statement, local_name) }
|
|
1515
|
+
end
|
|
1516
|
+
end
|
|
1517
|
+
|
|
1518
|
+
def source_proven_local_binding?(node, local_name)
|
|
1519
|
+
return false unless node.is_a?(Array)
|
|
1520
|
+
return source_proven_local_binding?(node[1], local_name) if ast_node?(node, :method_add_block)
|
|
1521
|
+
return false if nested_local_binding_scope?(node)
|
|
1522
|
+
return true if local_variable_field_named?(node, local_name)
|
|
1523
|
+
return true if required_parameter_named?(node, local_name)
|
|
1524
|
+
|
|
1525
|
+
children = node.first.is_a?(Symbol) ? node.drop(1) : node
|
|
1526
|
+
children.any? { |child| source_proven_local_binding?(child, local_name) }
|
|
1527
|
+
end
|
|
1528
|
+
|
|
1529
|
+
def nested_local_binding_scope?(node)
|
|
1530
|
+
%i[brace_block do_block lambda def defs].any? { |type| ast_node?(node, type) }
|
|
1531
|
+
end
|
|
1532
|
+
|
|
1533
|
+
def local_variable_field_named?(node, local_name)
|
|
1534
|
+
ast_node?(node, :var_field) && token_named?(node[1], local_name)
|
|
1535
|
+
end
|
|
1536
|
+
|
|
1537
|
+
def required_parameter_named?(node, local_name)
|
|
1538
|
+
ast_node?(node, :params) && node[1].is_a?(Array) &&
|
|
1539
|
+
node[1].any? { |parameter| token_named?(parameter, local_name) }
|
|
1540
|
+
end
|
|
1541
|
+
|
|
1542
|
+
def direct_self_helper_call?(node, helper_name)
|
|
1543
|
+
return false unless ast_node?(node, :call) || ast_node?(node, :command_call)
|
|
1544
|
+
|
|
1545
|
+
receiver = node[1]
|
|
1546
|
+
self_token = receiver[1] if ast_node?(receiver, :var_ref)
|
|
1547
|
+
token_type?(self_token, :@kw) && self_token[1] == "self" && token_named?(node[3], helper_name)
|
|
1548
|
+
end
|
|
1549
|
+
|
|
1550
|
+
def parenthesized_self_javascript_helper_call?(node, helper_name)
|
|
1551
|
+
return false unless helper_name == "javascript_pack_tag" && ast_node?(node, :method_add_arg)
|
|
1552
|
+
|
|
1553
|
+
argument_parentheses = node[2]
|
|
1554
|
+
ast_node?(argument_parentheses, :arg_paren) && direct_self_helper_call?(node[1], helper_name)
|
|
1555
|
+
end
|
|
1556
|
+
|
|
1557
|
+
def erb_helper_mentioned?(content, helper_name)
|
|
1558
|
+
helper_call_context_in_expressions?(content.scan(ERB_EXPRESSION_PATTERN), helper_name)
|
|
1559
|
+
end
|
|
1560
|
+
|
|
1561
|
+
def helper_call_context_in_expressions?(expressions, helper_name)
|
|
1562
|
+
expressions.any? do |(expression)|
|
|
1563
|
+
ripper_statements(expression).any? { |statement| helper_call_context_evidence?(statement, helper_name) }
|
|
1564
|
+
end
|
|
1565
|
+
end
|
|
1566
|
+
|
|
1567
|
+
def helper_call_context_evidence?(node, helper_name, ignored_bare_local: nil)
|
|
1568
|
+
return false unless node.is_a?(Array)
|
|
1569
|
+
return false if ignored_bare_local == helper_name && bare_unqualified_helper_vcall?(node, helper_name)
|
|
1570
|
+
return true if helper_call_node?(node, helper_name)
|
|
1571
|
+
return true if reflective_helper_dispatch_may_call?(node, helper_name)
|
|
1572
|
+
|
|
1573
|
+
children = node.first.is_a?(Symbol) ? node.drop(1) : node
|
|
1574
|
+
children.any? { |child| helper_call_context_evidence?(child, helper_name, ignored_bare_local:) }
|
|
1575
|
+
end
|
|
1576
|
+
|
|
1577
|
+
def helper_call_node?(node, helper_name)
|
|
1578
|
+
method_token = call_method_token(node)
|
|
1579
|
+
token_named?(method_token, helper_name)
|
|
1580
|
+
end
|
|
1581
|
+
|
|
1582
|
+
def reflective_helper_dispatch_may_call?(node, helper_name)
|
|
1583
|
+
arguments = reflective_dispatch_arguments(node)
|
|
1584
|
+
return false unless arguments&.any?
|
|
1585
|
+
|
|
1586
|
+
dispatched_name = static_string_value(arguments.first) || static_symbol_value(arguments.first)
|
|
1587
|
+
dispatched_name.nil? || dispatched_name == helper_name
|
|
1588
|
+
end
|
|
1589
|
+
|
|
1590
|
+
def reflective_dispatch_arguments(node)
|
|
1591
|
+
if ast_node?(node, :method_add_arg)
|
|
1592
|
+
return unless reflective_dispatch_call?(node[1])
|
|
1593
|
+
|
|
1594
|
+
argument_parentheses = node[2]
|
|
1595
|
+
return unless ast_node?(argument_parentheses, :arg_paren)
|
|
1596
|
+
|
|
1597
|
+
static_argument_values(argument_parentheses[1])
|
|
1598
|
+
elsif ast_node?(node, :command) && reflective_dispatch_call?(node)
|
|
1599
|
+
static_argument_values(node[2])
|
|
1600
|
+
elsif reflective_command_call?(node)
|
|
1601
|
+
static_argument_values(node[4])
|
|
1602
|
+
end
|
|
1603
|
+
end
|
|
1604
|
+
|
|
1605
|
+
def reflective_command_call?(node)
|
|
1606
|
+
return false unless ast_node?(node, :command_call)
|
|
1607
|
+
|
|
1608
|
+
method_token = node[3]
|
|
1609
|
+
token_type?(method_token, :@ident) && REFLECTIVE_HELPER_DISPATCH_METHODS.include?(method_token[1])
|
|
1610
|
+
end
|
|
1611
|
+
|
|
1612
|
+
def reflective_dispatch_call?(node)
|
|
1613
|
+
method_token = call_method_token(node)
|
|
1614
|
+
token_type?(method_token, :@ident) && REFLECTIVE_HELPER_DISPATCH_METHODS.include?(method_token[1])
|
|
1615
|
+
end
|
|
1616
|
+
|
|
1617
|
+
def call_method_token(node)
|
|
1618
|
+
case node&.first
|
|
1619
|
+
when :vcall, :fcall, :command
|
|
1620
|
+
node[1]
|
|
1621
|
+
when :call
|
|
1622
|
+
node[3]
|
|
1623
|
+
end
|
|
1624
|
+
end
|
|
1625
|
+
|
|
1626
|
+
def direct_unqualified_helper_call?(node, helper_name)
|
|
1627
|
+
return token_named?(node[1], helper_name) if ast_node?(node, :vcall) || ast_node?(node, :command)
|
|
1628
|
+
|
|
1629
|
+
ast_node?(node, :method_add_arg) && fcall_named?(node[1], helper_name)
|
|
1630
|
+
end
|
|
1631
|
+
|
|
1632
|
+
def bare_unqualified_helper_vcall?(node, helper_name)
|
|
1633
|
+
ast_node?(node, :vcall) && token_named?(node[1], helper_name)
|
|
1634
|
+
end
|
|
1635
|
+
|
|
1636
|
+
def discarded_top_level_helper_call?(node, helper_name)
|
|
1637
|
+
return true if direct_unqualified_helper_call?(node, helper_name)
|
|
1638
|
+
return true if direct_self_helper_call?(node, helper_name)
|
|
1639
|
+
|
|
1640
|
+
ast_node?(node, :method_add_arg) && direct_self_helper_call?(node[1], helper_name)
|
|
1641
|
+
end
|
|
1642
|
+
|
|
1643
|
+
def uncommented_template_content(content)
|
|
1644
|
+
utf8_content = content.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
1645
|
+
loop do
|
|
1646
|
+
uncommented_content = utf8_content.gsub(TEMPLATE_COMMENT_PATTERN, "")
|
|
1647
|
+
return uncommented_content if uncommented_content == utf8_content
|
|
1648
|
+
|
|
1649
|
+
utf8_content = uncommented_content
|
|
1650
|
+
end
|
|
1651
|
+
rescue EncodingError
|
|
1652
|
+
""
|
|
1653
|
+
end
|
|
1654
|
+
|
|
1655
|
+
def html_comment_with_executable_erb?(content)
|
|
1656
|
+
utf8_content = content.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
1657
|
+
loop do
|
|
1658
|
+
return true if utf8_content.scan(TEMPLATE_HTML_COMMENT_PATTERN).any? do |comment|
|
|
1659
|
+
comment.match?(ERB_EXPRESSION_PATTERN)
|
|
1660
|
+
end
|
|
1661
|
+
|
|
1662
|
+
uncommented_content = utf8_content.gsub(TEMPLATE_COMMENT_PATTERN, "")
|
|
1663
|
+
return false if uncommented_content == utf8_content
|
|
1664
|
+
|
|
1665
|
+
utf8_content = uncommented_content
|
|
1666
|
+
end
|
|
1667
|
+
rescue EncodingError
|
|
1668
|
+
true
|
|
1669
|
+
end
|
|
1670
|
+
|
|
1671
|
+
def auto_loaded_component_names(content)
|
|
1672
|
+
return [] if separate_erb_control_flow?(content)
|
|
1673
|
+
|
|
1674
|
+
auto_loaded_component_arguments(content).filter_map do |arguments|
|
|
1675
|
+
next unless statically_auto_loaded_component?(arguments)
|
|
1676
|
+
|
|
1677
|
+
static_string_value(arguments.first)&.camelize
|
|
1678
|
+
end.uniq
|
|
1679
|
+
end
|
|
1680
|
+
|
|
1681
|
+
def auto_loaded_component_arguments(content)
|
|
1682
|
+
expressions = content.scan(ERB_OUTPUT_EXPRESSION_PATTERN).map(&:first)
|
|
1683
|
+
assignments = content.scan(ERB_NON_OUTPUT_EXPRESSION_PATTERN).map(&:first)
|
|
1684
|
+
component_arguments = expressions.filter_map { |expression| static_react_component_arguments(expression) }
|
|
1685
|
+
component_arguments.concat(
|
|
1686
|
+
assignments.filter_map { |expression| static_react_component_assignment_arguments(expression) }
|
|
1687
|
+
)
|
|
1688
|
+
end
|
|
1689
|
+
|
|
1690
|
+
def separate_erb_control_flow?(content)
|
|
1691
|
+
content.scan(ERB_NON_OUTPUT_EXPRESSION_PATTERN).any? do |(expression)|
|
|
1692
|
+
next true unless Ripper.sexp(expression)
|
|
1693
|
+
|
|
1694
|
+
Ripper.lex(expression).any? do |(_position, event, token)|
|
|
1695
|
+
event == :on_kw && ERB_CONTROL_FLOW_KEYWORDS.include?(token)
|
|
1696
|
+
end
|
|
1697
|
+
end
|
|
1698
|
+
end
|
|
1699
|
+
|
|
1700
|
+
def statically_auto_loaded_component?(arguments)
|
|
1701
|
+
options = static_component_options(arguments.drop(1))
|
|
1702
|
+
return false unless options
|
|
1703
|
+
return static_global_auto_load_bundle == true if options == :omitted
|
|
1704
|
+
|
|
1705
|
+
component_option = static_boolean_keyword(options, "auto_load_bundle")
|
|
1706
|
+
return component_option == true unless component_option == :omitted
|
|
1707
|
+
|
|
1708
|
+
static_global_auto_load_bundle == true
|
|
1709
|
+
end
|
|
1710
|
+
|
|
1711
|
+
def static_react_component_arguments(expression)
|
|
1712
|
+
statements = ripper_statements(expression)
|
|
1713
|
+
return unless statements.one?
|
|
1714
|
+
|
|
1715
|
+
%w[react_component react_component_hash].filter_map do |helper_name|
|
|
1716
|
+
static_named_component_arguments(statements.first, helper_name)
|
|
1717
|
+
end.first
|
|
1718
|
+
end
|
|
1719
|
+
|
|
1720
|
+
def static_react_component_assignment_arguments(expression)
|
|
1721
|
+
statements = ripper_statements(expression)
|
|
1722
|
+
return unless statements.one?
|
|
1723
|
+
|
|
1724
|
+
assignment = statements.first
|
|
1725
|
+
return unless ast_node?(assignment, :assign)
|
|
1726
|
+
|
|
1727
|
+
field = assignment[1]
|
|
1728
|
+
return unless ast_node?(field, :var_field) && token_type?(field[1], :@ident)
|
|
1729
|
+
|
|
1730
|
+
%w[react_component react_component_hash].filter_map do |helper_name|
|
|
1731
|
+
static_named_component_arguments(assignment[2], helper_name)
|
|
1732
|
+
end.first
|
|
1733
|
+
end
|
|
1734
|
+
|
|
1735
|
+
def static_named_component_arguments(node, helper_name)
|
|
1736
|
+
node = static_self_component_hash_call(node) || node if helper_name == "react_component_hash"
|
|
1737
|
+
call_arguments = static_call_arguments(node, helper_name) || static_self_call_arguments(node, helper_name)
|
|
1738
|
+
arguments = static_argument_values(call_arguments)
|
|
1739
|
+
component_name = static_string_value(arguments.first) if arguments&.any?
|
|
1740
|
+
arguments if component_name&.match?(/\A[A-Za-z0-9_:]+\z/)
|
|
1741
|
+
end
|
|
1742
|
+
|
|
1743
|
+
def static_self_call_arguments(node, helper_name)
|
|
1744
|
+
return unless ast_node?(node, :method_add_arg) && direct_self_helper_call?(node[1], helper_name)
|
|
1745
|
+
|
|
1746
|
+
argument_parentheses = node[2]
|
|
1747
|
+
argument_parentheses[1] if ast_node?(argument_parentheses, :arg_paren)
|
|
1748
|
+
end
|
|
1749
|
+
|
|
1750
|
+
def static_self_component_hash_call(node)
|
|
1751
|
+
return unless ast_node?(node, :aref)
|
|
1752
|
+
|
|
1753
|
+
call = node[1]
|
|
1754
|
+
return unless ast_node?(call, :method_add_arg) && direct_self_helper_call?(call[1], "react_component_hash")
|
|
1755
|
+
|
|
1756
|
+
access_arguments = static_argument_values(node[2])
|
|
1757
|
+
call if access_arguments&.one? && static_string_value(access_arguments.first) == "componentHtml"
|
|
1758
|
+
end
|
|
1759
|
+
|
|
1760
|
+
def ripper_statements(source)
|
|
1761
|
+
program = Ripper.sexp(source)
|
|
1762
|
+
statements = program[1] if ast_node?(program, :program)
|
|
1763
|
+
statements.is_a?(Array) ? statements : []
|
|
1764
|
+
end
|
|
1765
|
+
|
|
1766
|
+
def static_call_arguments(node, method_name)
|
|
1767
|
+
return node[2] if ast_node?(node, :command) && token_named?(node[1], method_name)
|
|
1768
|
+
return unless ast_node?(node, :method_add_arg) && fcall_named?(node[1], method_name)
|
|
1769
|
+
|
|
1770
|
+
argument_parentheses = node[2]
|
|
1771
|
+
argument_parentheses[1] if ast_node?(argument_parentheses, :arg_paren)
|
|
1772
|
+
end
|
|
1773
|
+
|
|
1774
|
+
def static_argument_values(node)
|
|
1775
|
+
return unless ast_node?(node, :args_add_block) && node[2] == false
|
|
1776
|
+
|
|
1777
|
+
node[1] if node[1].is_a?(Array)
|
|
1778
|
+
end
|
|
1779
|
+
|
|
1780
|
+
def ast_node?(node, type)
|
|
1781
|
+
node.is_a?(Array) && node.first == type
|
|
1782
|
+
end
|
|
1783
|
+
|
|
1784
|
+
def token_type?(node, type)
|
|
1785
|
+
ast_node?(node, type) && node[1].is_a?(String)
|
|
1786
|
+
end
|
|
1787
|
+
|
|
1788
|
+
def fcall_named?(node, name)
|
|
1789
|
+
ast_node?(node, :fcall) && token_named?(node[1], name)
|
|
1790
|
+
end
|
|
1791
|
+
|
|
1792
|
+
def token_named?(node, name)
|
|
1793
|
+
token_type?(node, :@ident) && node[1] == name
|
|
1794
|
+
end
|
|
1795
|
+
|
|
1796
|
+
def static_string_value(node)
|
|
1797
|
+
return unless ast_node?(node, :string_literal)
|
|
1798
|
+
|
|
1799
|
+
content = node[1]
|
|
1800
|
+
return unless ast_node?(content, :string_content)
|
|
1801
|
+
|
|
1802
|
+
parts = content.drop(1)
|
|
1803
|
+
return unless parts.all? { |part| token_type?(part, :@tstring_content) }
|
|
1804
|
+
|
|
1805
|
+
parts.map { |part| part[1] }.join
|
|
1806
|
+
end
|
|
1807
|
+
|
|
1808
|
+
def static_boolean_keyword(associations, keyword)
|
|
1809
|
+
matches = associations.select do |association|
|
|
1810
|
+
static_label_association?(association, "#{keyword}:")
|
|
1811
|
+
end
|
|
1812
|
+
return :omitted if matches.empty?
|
|
1813
|
+
return unless matches.one?
|
|
1814
|
+
|
|
1815
|
+
static_boolean_value(matches.first[2])
|
|
1816
|
+
end
|
|
1817
|
+
|
|
1818
|
+
def static_component_options(arguments)
|
|
1819
|
+
return :omitted if arguments.empty?
|
|
1820
|
+
return unless arguments.one?
|
|
1821
|
+
|
|
1822
|
+
associations = static_hash_associations(arguments.first)
|
|
1823
|
+
associations if associations&.all? { |association| static_data_association?(association) }
|
|
1824
|
+
end
|
|
1825
|
+
|
|
1826
|
+
def static_data_association?(node)
|
|
1827
|
+
static_label_association?(node) && static_data_value?(node[2])
|
|
1828
|
+
end
|
|
1829
|
+
|
|
1830
|
+
def static_data_value?(node)
|
|
1831
|
+
return true if static_scalar_value?(node)
|
|
1832
|
+
|
|
1833
|
+
case node&.first
|
|
1834
|
+
when :array
|
|
1835
|
+
static_data_array?(node[1])
|
|
1836
|
+
when :hash
|
|
1837
|
+
static_data_hash?(node)
|
|
1838
|
+
when :symbol_literal
|
|
1839
|
+
static_symbol_literal?(node)
|
|
1840
|
+
when :unary
|
|
1841
|
+
static_numeric_unary_literal?(node)
|
|
1842
|
+
else
|
|
1843
|
+
false
|
|
1844
|
+
end
|
|
1845
|
+
end
|
|
1846
|
+
|
|
1847
|
+
def static_data_array?(values)
|
|
1848
|
+
values.nil? || (values.is_a?(Array) && values.all? { |value| static_data_value?(value) })
|
|
1849
|
+
end
|
|
1850
|
+
|
|
1851
|
+
def static_data_hash?(node)
|
|
1852
|
+
associations = static_hash_associations(node)
|
|
1853
|
+
associations&.all? { |association| static_data_association?(association) }
|
|
1854
|
+
end
|
|
1855
|
+
|
|
1856
|
+
def static_symbol_literal?(node)
|
|
1857
|
+
!static_symbol_value(node).nil?
|
|
1858
|
+
end
|
|
1859
|
+
|
|
1860
|
+
def static_symbol_value(node)
|
|
1861
|
+
symbol = node[1] if ast_node?(node, :symbol_literal)
|
|
1862
|
+
token = symbol[1] if ast_node?(symbol, :symbol)
|
|
1863
|
+
token[1] if token_type?(token, :@ident) || token_type?(token, :@op)
|
|
1864
|
+
end
|
|
1865
|
+
|
|
1866
|
+
def static_numeric_unary_literal?(node)
|
|
1867
|
+
%i[+@ -@].include?(node[1]) && (token_type?(node[2], :@int) || token_type?(node[2], :@float))
|
|
1868
|
+
end
|
|
1869
|
+
|
|
1870
|
+
def static_scalar_value?(node)
|
|
1871
|
+
return true unless static_string_value(node).nil?
|
|
1872
|
+
return true if token_type?(node, :@int) || token_type?(node, :@float)
|
|
1873
|
+
|
|
1874
|
+
token = node[1] if ast_node?(node, :var_ref)
|
|
1875
|
+
token_type?(token, :@ivar) || (token_type?(token, :@kw) && %w[true false nil].include?(token[1]))
|
|
1876
|
+
end
|
|
1877
|
+
|
|
1878
|
+
def static_hash_associations(node)
|
|
1879
|
+
return node[1] if ast_node?(node, :bare_assoc_hash)
|
|
1880
|
+
return unless ast_node?(node, :hash)
|
|
1881
|
+
|
|
1882
|
+
association_list = node[1]
|
|
1883
|
+
return [] unless association_list
|
|
1884
|
+
|
|
1885
|
+
association_list[1] if ast_node?(association_list, :assoclist_from_args)
|
|
1886
|
+
end
|
|
1887
|
+
|
|
1888
|
+
def static_label_association?(node, label = nil)
|
|
1889
|
+
return false unless ast_node?(node, :assoc_new) && ast_node?(node[1], :@label)
|
|
1890
|
+
|
|
1891
|
+
label.nil? || node[1][1] == label
|
|
1892
|
+
end
|
|
1893
|
+
|
|
1894
|
+
def static_global_auto_load_bundle
|
|
1895
|
+
return @static_global_auto_load_bundle if defined?(@static_global_auto_load_bundle)
|
|
1896
|
+
return (@static_global_auto_load_bundle = nil) unless File.exist?(REACT_ON_RAILS_INITIALIZER_PATH)
|
|
1897
|
+
|
|
1898
|
+
source = File.read(REACT_ON_RAILS_INITIALIZER_PATH)
|
|
1899
|
+
@static_global_auto_load_bundle = static_configured_boolean(source, "auto_load_bundle")
|
|
1900
|
+
rescue StandardError
|
|
1901
|
+
@static_global_auto_load_bundle = nil
|
|
1902
|
+
end
|
|
1903
|
+
|
|
1904
|
+
def static_configured_boolean(source, setting_name)
|
|
1905
|
+
configure_block = unique_top_level_configure_block(source)
|
|
1906
|
+
return unless configure_block
|
|
1907
|
+
|
|
1908
|
+
config_name = configure_block_parameter_name(configure_block)
|
|
1909
|
+
block_body = configure_block.dig(2, 2)
|
|
1910
|
+
direct_assignments = static_direct_config_assignments(block_body, config_name)
|
|
1911
|
+
return unless direct_assignments
|
|
1912
|
+
return unless ast_identifier_count(block_body, setting_name) == 1
|
|
1913
|
+
|
|
1914
|
+
assignments = direct_assignments.select do |statement|
|
|
1915
|
+
config_assignment?(statement, config_name, setting_name)
|
|
1916
|
+
end
|
|
1917
|
+
static_boolean_value(assignments.first[2]) if assignments.one?
|
|
1918
|
+
end
|
|
1919
|
+
|
|
1920
|
+
def unique_top_level_configure_block(source)
|
|
1921
|
+
source_statements = ripper_statements(source)
|
|
1922
|
+
return unless source_statements.one?
|
|
1923
|
+
|
|
1924
|
+
configure_block = source_statements.first
|
|
1925
|
+
configure_block if react_on_rails_configure_block?(configure_block)
|
|
1926
|
+
end
|
|
1927
|
+
|
|
1928
|
+
def static_direct_config_assignments(block_body, config_name)
|
|
1929
|
+
statements = static_body_statements(block_body)
|
|
1930
|
+
return unless config_name && statements
|
|
1931
|
+
return unless statements.all? { |statement| direct_config_assignment?(statement, config_name) }
|
|
1932
|
+
return unless statements.all? { |statement| static_literal_data_value?(statement[2]) }
|
|
1933
|
+
return unless ast_identifier_count(block_body, config_name) == statements.length
|
|
1934
|
+
|
|
1935
|
+
statements
|
|
1936
|
+
end
|
|
1937
|
+
|
|
1938
|
+
def static_literal_data_value?(node)
|
|
1939
|
+
return true if static_literal_scalar_value?(node)
|
|
1940
|
+
return static_literal_array?(node[1]) if ast_node?(node, :array)
|
|
1941
|
+
return static_literal_hash?(node) if ast_node?(node, :hash)
|
|
1942
|
+
return static_symbol_literal?(node) if ast_node?(node, :symbol_literal)
|
|
1943
|
+
return static_numeric_unary_literal?(node) if ast_node?(node, :unary)
|
|
1944
|
+
|
|
1945
|
+
false
|
|
1946
|
+
end
|
|
1947
|
+
|
|
1948
|
+
def static_literal_scalar_value?(node)
|
|
1949
|
+
return true unless static_string_value(node).nil?
|
|
1950
|
+
return true if token_type?(node, :@int) || token_type?(node, :@float)
|
|
1951
|
+
|
|
1952
|
+
keyword = node[1] if ast_node?(node, :var_ref)
|
|
1953
|
+
token_type?(keyword, :@kw) && %w[true false nil].include?(keyword[1])
|
|
1954
|
+
end
|
|
1955
|
+
|
|
1956
|
+
def static_literal_array?(values)
|
|
1957
|
+
values.nil? || (values.is_a?(Array) && values.all? { |value| static_literal_data_value?(value) })
|
|
1958
|
+
end
|
|
1959
|
+
|
|
1960
|
+
def static_literal_hash?(node)
|
|
1961
|
+
associations = static_hash_associations(node)
|
|
1962
|
+
associations&.all? do |association|
|
|
1963
|
+
static_label_association?(association) && static_literal_data_value?(association[2])
|
|
1964
|
+
end
|
|
1965
|
+
end
|
|
1966
|
+
|
|
1967
|
+
def ast_identifier_count(node, identifier)
|
|
1968
|
+
return 0 unless node.is_a?(Array)
|
|
1969
|
+
|
|
1970
|
+
count = token_named?(node, identifier) ? 1 : 0
|
|
1971
|
+
children = node.first.is_a?(Symbol) ? node.drop(1) : node
|
|
1972
|
+
count + children.sum { |child| ast_identifier_count(child, identifier) }
|
|
1973
|
+
end
|
|
1974
|
+
|
|
1975
|
+
def configure_block_parameter_name(node)
|
|
1976
|
+
block_var = node.dig(2, 1)
|
|
1977
|
+
parameters = block_var[1] if ast_node?(block_var, :block_var)
|
|
1978
|
+
required = parameters[1] if ast_node?(parameters, :params)
|
|
1979
|
+
required.first[1] if required&.one? && token_type?(required.first, :@ident)
|
|
1980
|
+
end
|
|
1981
|
+
|
|
1982
|
+
def react_on_rails_configure_block?(node)
|
|
1983
|
+
return false unless ast_node?(node, :method_add_block)
|
|
1984
|
+
|
|
1985
|
+
call = node[1]
|
|
1986
|
+
block = node[2]
|
|
1987
|
+
ast_node?(call, :call) && static_constant_reference(call[1]) == "ReactOnRails" &&
|
|
1988
|
+
token_named?(call[3], "configure") && ast_node?(block, :do_block)
|
|
1989
|
+
end
|
|
1990
|
+
|
|
1991
|
+
def static_constant_reference(node)
|
|
1992
|
+
static_constant_token_value(node[1]) if ast_node?(node, :var_ref)
|
|
1993
|
+
end
|
|
1994
|
+
|
|
1995
|
+
def config_assignment?(node, config_name, setting_name)
|
|
1996
|
+
return false unless direct_config_assignment?(node, config_name)
|
|
1997
|
+
|
|
1998
|
+
field = node[1]
|
|
1999
|
+
token_named?(field[3], setting_name)
|
|
2000
|
+
end
|
|
2001
|
+
|
|
2002
|
+
def direct_config_assignment?(node, config_name)
|
|
2003
|
+
return false unless ast_node?(node, :assign)
|
|
2004
|
+
|
|
2005
|
+
field = node[1]
|
|
2006
|
+
ast_node?(field, :field) && static_identifier_reference(field[1]) == config_name
|
|
2007
|
+
end
|
|
2008
|
+
|
|
2009
|
+
def static_identifier_reference(node)
|
|
2010
|
+
token = node[1] if ast_node?(node, :var_ref)
|
|
2011
|
+
token[1] if token_type?(token, :@ident)
|
|
2012
|
+
end
|
|
2013
|
+
|
|
2014
|
+
def static_boolean_value(node)
|
|
2015
|
+
return unless ast_node?(node, :var_ref)
|
|
2016
|
+
|
|
2017
|
+
keyword = node[1]
|
|
2018
|
+
return true if token_type?(keyword, :@kw) && keyword[1] == "true"
|
|
2019
|
+
return false if token_type?(keyword, :@kw) && keyword[1] == "false"
|
|
2020
|
+
|
|
2021
|
+
nil
|
|
2022
|
+
end
|
|
2023
|
+
|
|
2024
|
+
def shakapacker_manifest_data
|
|
2025
|
+
return @shakapacker_manifest_data if defined?(@shakapacker_manifest_data)
|
|
2026
|
+
|
|
2027
|
+
require "shakapacker"
|
|
2028
|
+
manifest_path = ::Shakapacker.config.manifest_path
|
|
2029
|
+
@shakapacker_manifest_data = JSON.parse(manifest_path.read) if manifest_path&.file?
|
|
2030
|
+
rescue LoadError, StandardError
|
|
2031
|
+
@shakapacker_manifest_data = nil
|
|
2032
|
+
end
|
|
2033
|
+
|
|
1269
2034
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
1270
2035
|
def check_server_rendering_engine
|
|
1271
2036
|
return unless defined?(ReactOnRails)
|
|
@@ -4301,26 +5066,34 @@ module ReactOnRails
|
|
|
4301
5066
|
RSC_PACKAGE_NAME = "react-on-rails-rsc"
|
|
4302
5067
|
RSC_MINIMUM_PACKAGE_VERSION = "19.2.1"
|
|
4303
5068
|
RSC_SUPPORTED_PACKAGE_MAJOR = 19
|
|
4304
|
-
#
|
|
4305
|
-
|
|
4306
|
-
|
|
5069
|
+
# Keep in sync with packages/react-on-rails-pro-node-renderer/src/shared/rscPeerSupport.ts.
|
|
5070
|
+
# Each row covers react-on-rails-rsc patches rsc_min_patch..rsc_max_patch (nil = open-ended)
|
|
5071
|
+
# on rsc_minor. React must match the Flight line the RSC package bundles:
|
|
5072
|
+
# 19.2.x ships Flight 19.2, published 19.3.0 ships Flight 19.2.8 (React on Rails 17.1.0),
|
|
5073
|
+
# and 19.3.1+ (starting with 19.3.1-rc.0) ships Flight 19.3.0.
|
|
5074
|
+
RSC_REACT_SUPPORT_RANGES = [
|
|
5075
|
+
{ rsc_minor: 2, rsc_min_patch: 1, rsc_max_patch: nil, minor: 2, min_patch: 7 },
|
|
5076
|
+
{ rsc_minor: 3, rsc_min_patch: 0, rsc_max_patch: 0, minor: 2, min_patch: 8 },
|
|
5077
|
+
{ rsc_minor: 3, rsc_min_patch: 1, rsc_max_patch: nil, minor: 3, min_patch: 0 }
|
|
5078
|
+
].freeze
|
|
5079
|
+
RSC_SUPPORTED_PACKAGE_MINORS = RSC_REACT_SUPPORT_RANGES.map { |range| range.fetch(:rsc_minor) }.uniq.freeze
|
|
4307
5080
|
RSC_SUPPORTED_PACKAGE_LINE = RSC_SUPPORTED_PACKAGE_MINORS.map do |minor|
|
|
4308
|
-
|
|
5081
|
+
min_patch = RSC_REACT_SUPPORT_RANGES.select { |range| range.fetch(:rsc_minor) == minor }
|
|
5082
|
+
.map { |range| range.fetch(:rsc_min_patch) }.min
|
|
5083
|
+
line = "#{RSC_SUPPORTED_PACKAGE_MAJOR}.#{minor}.x"
|
|
5084
|
+
min_patch.positive? ? "#{line} starting at #{RSC_SUPPORTED_PACKAGE_MAJOR}.#{minor}.#{min_patch}" : line
|
|
4309
5085
|
end.join(" or ")
|
|
4310
5086
|
RSC_PACKAGE_INSTALL_VERSION = ReactOnRails::Generators::JsDependencyManager::RSC_PACKAGE_VERSION_PIN
|
|
4311
|
-
#
|
|
4312
|
-
#
|
|
4313
|
-
RSC_MINIMUM_PACKAGE_PRERELEASE_VERSION =
|
|
4314
|
-
RSC_PACKAGE_INSTALL_VERSION.include?("-") ? RSC_PACKAGE_INSTALL_VERSION : nil
|
|
5087
|
+
# Admits the 19.3.1-rc.x soak. The generator pin stays on stable 19.2.1.
|
|
5088
|
+
# Remove once react-on-rails-rsc 19.3.1 ships stable.
|
|
5089
|
+
RSC_MINIMUM_PACKAGE_PRERELEASE_VERSION = "19.3.1-rc.0"
|
|
4315
5090
|
RSC_MINIMUM_REACT_VERSION = "19.2.7"
|
|
4316
5091
|
RSC_MINIMUM_REACT_VERSION_TUPLE = RSC_MINIMUM_REACT_VERSION.split(".").map(&:to_i).freeze
|
|
4317
5092
|
RSC_SUPPORTED_REACT_MAJOR = RSC_MINIMUM_REACT_VERSION_TUPLE.fetch(0)
|
|
4318
|
-
RSC_SUPPORTED_REACT_MINORS =
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
"#{RSC_SUPPORTED_REACT_MAJOR}.#{minor}.x"
|
|
4323
|
-
end.join(" or ")
|
|
5093
|
+
RSC_SUPPORTED_REACT_MINORS = RSC_REACT_SUPPORT_RANGES.map { |range| range.fetch(:minor) }.uniq.freeze
|
|
5094
|
+
RSC_SUPPORTED_REACT_LINE = RSC_REACT_SUPPORT_RANGES.map do |range|
|
|
5095
|
+
"#{RSC_SUPPORTED_REACT_MAJOR}.#{range.fetch(:minor)}.x"
|
|
5096
|
+
end.uniq.join(" or ")
|
|
4324
5097
|
RSC_DIST_TAGS_TO_CHECK = %w[next rc].freeze
|
|
4325
5098
|
NPM_VIEW_FETCH_TIMEOUT_MS = 5_000
|
|
4326
5099
|
NPM_VIEW_FETCH_TIMEOUT_SECONDS = NPM_VIEW_FETCH_TIMEOUT_MS / 1000.0
|
|
@@ -4608,14 +5381,17 @@ module ReactOnRails
|
|
|
4608
5381
|
return true unless check_rsc_package_minimum_version(rsc_package)
|
|
4609
5382
|
|
|
4610
5383
|
unless rsc_package_declares_react_peer_dependencies?(rsc_package)
|
|
4611
|
-
return true unless check_rsc_supported_react_packages_for_package(rsc_package, react_version)
|
|
4612
|
-
|
|
4613
5384
|
checker.add_warning(<<~MSG.strip)
|
|
4614
5385
|
⚠️ #{RSC_PACKAGE_NAME} #{rsc_package['version']} does not declare React peer dependencies.
|
|
4615
5386
|
|
|
4616
|
-
|
|
5387
|
+
Checking React against the React on Rails Pro support window for this package instead.
|
|
4617
5388
|
MSG
|
|
4618
|
-
|
|
5389
|
+
if check_rsc_supported_react_packages_for_package(rsc_package, react_version)
|
|
5390
|
+
checker.add_success(
|
|
5391
|
+
"✅ React #{react_version} is compatible with #{RSC_PACKAGE_NAME} #{rsc_package['version']}"
|
|
5392
|
+
)
|
|
5393
|
+
end
|
|
5394
|
+
return true
|
|
4619
5395
|
end
|
|
4620
5396
|
|
|
4621
5397
|
peer_compatible = check_rsc_package_peer_compatibility(rsc_package, react_version)
|
|
@@ -4631,27 +5407,40 @@ module ReactOnRails
|
|
|
4631
5407
|
rsc_version = rsc_package["version"].to_s
|
|
4632
5408
|
return true if rsc_package_version_at_or_above_minimum?(rsc_version)
|
|
4633
5409
|
|
|
4634
|
-
react_requirements = RSC_REACT_MINIMUM_BY_PACKAGE_MINOR.map do |minor, minimum|
|
|
4635
|
-
"RSC #{RSC_SUPPORTED_PACKAGE_MAJOR}.#{minor}.x requires stable React/React DOM ~#{minimum}"
|
|
4636
|
-
end.join("; ")
|
|
4637
5410
|
prerelease_requirement = if RSC_MINIMUM_PACKAGE_PRERELEASE_VERSION.present?
|
|
4638
5411
|
"\n(or #{RSC_MINIMUM_PACKAGE_PRERELEASE_VERSION} during the RC soak)"
|
|
4639
5412
|
else
|
|
4640
5413
|
""
|
|
4641
5414
|
end
|
|
4642
5415
|
|
|
5416
|
+
rsc_install, react_install = rsc_package_floor_install_versions(rsc_version)
|
|
5417
|
+
|
|
4643
5418
|
checker.add_error(<<~MSG.strip)
|
|
4644
5419
|
🚫 #{RSC_PACKAGE_NAME} #{rsc_version.presence || 'unknown'} is not supported by React on Rails Pro 17 RSC.
|
|
4645
5420
|
|
|
4646
5421
|
React on Rails Pro 17 requires #{RSC_PACKAGE_NAME} >= #{RSC_MINIMUM_PACKAGE_VERSION}#{prerelease_requirement}
|
|
4647
|
-
on the supported #{RSC_SUPPORTED_PACKAGE_LINE} package line
|
|
4648
|
-
#{
|
|
5422
|
+
on the supported #{RSC_SUPPORTED_PACKAGE_LINE} package line
|
|
5423
|
+
with React/React DOM #{react_install}+.
|
|
4649
5424
|
|
|
4650
|
-
Fix: npm install react
|
|
5425
|
+
Fix: npm install react@~#{react_install} react-dom@~#{react_install} #{RSC_PACKAGE_NAME}@#{rsc_install} --save-exact
|
|
4651
5426
|
MSG
|
|
4652
5427
|
false
|
|
4653
5428
|
end
|
|
4654
5429
|
|
|
5430
|
+
# Only the 19.3.0 transition has a known published stable successor. Compatibility does not establish
|
|
5431
|
+
# publication for other rejected prereleases; use the generator pin for those versions.
|
|
5432
|
+
# Returns [rsc_version_to_install, react_version_to_install].
|
|
5433
|
+
def rsc_package_floor_install_versions(rsc_version)
|
|
5434
|
+
if npm_prerelease(rsc_version).present? && npm_version_tuple(rsc_version) == [19, 3, 0]
|
|
5435
|
+
stable_version = npm_version_tuple(rsc_version).join(".")
|
|
5436
|
+
if rsc_stable_package_version_supported?(stable_version)
|
|
5437
|
+
return [stable_version, recommended_react_install_version_for_rsc_package(stable_version)]
|
|
5438
|
+
end
|
|
5439
|
+
end
|
|
5440
|
+
|
|
5441
|
+
[RSC_PACKAGE_INSTALL_VERSION, RSC_MINIMUM_REACT_VERSION]
|
|
5442
|
+
end
|
|
5443
|
+
|
|
4655
5444
|
def rsc_package_version_at_or_above_minimum?(rsc_version)
|
|
4656
5445
|
return false if rsc_version.blank?
|
|
4657
5446
|
return true if rsc_stable_package_version_supported?(rsc_version)
|
|
@@ -4664,19 +5453,28 @@ module ReactOnRails
|
|
|
4664
5453
|
def rsc_stable_package_version_supported?(rsc_version)
|
|
4665
5454
|
return false if npm_prerelease(rsc_version).present?
|
|
4666
5455
|
|
|
4667
|
-
major,
|
|
5456
|
+
major, = npm_version_tuple(rsc_version)
|
|
4668
5457
|
major == RSC_SUPPORTED_PACKAGE_MAJOR &&
|
|
4669
|
-
|
|
5458
|
+
rsc_react_support_range_for(rsc_version) &&
|
|
4670
5459
|
npm_version_compare(rsc_version, RSC_MINIMUM_PACKAGE_VERSION) >= 0
|
|
4671
5460
|
end
|
|
4672
5461
|
|
|
5462
|
+
def rsc_react_support_range_for(rsc_version)
|
|
5463
|
+
_rsc_major, rsc_minor, rsc_patch = npm_version_tuple(rsc_version)
|
|
5464
|
+
RSC_REACT_SUPPORT_RANGES.find do |range|
|
|
5465
|
+
max_patch = range.fetch(:rsc_max_patch)
|
|
5466
|
+
range.fetch(:rsc_minor) == rsc_minor &&
|
|
5467
|
+
rsc_patch >= range.fetch(:rsc_min_patch) &&
|
|
5468
|
+
(max_patch.nil? || rsc_patch <= max_patch)
|
|
5469
|
+
end
|
|
5470
|
+
end
|
|
5471
|
+
|
|
4673
5472
|
def unsupported_rsc_react_version?(react_version)
|
|
4674
5473
|
!supported_rsc_react_version?(react_version)
|
|
4675
5474
|
end
|
|
4676
5475
|
|
|
4677
5476
|
def supported_rsc_react_version?(react_version)
|
|
4678
|
-
|
|
4679
|
-
supported_rsc_react_line?(react_version) &&
|
|
5477
|
+
supported_rsc_react_line?(react_version) &&
|
|
4680
5478
|
!npm_version_less_than?(react_version, RSC_MINIMUM_REACT_VERSION)
|
|
4681
5479
|
end
|
|
4682
5480
|
|
|
@@ -4701,25 +5499,51 @@ module ReactOnRails
|
|
|
4701
5499
|
|
|
4702
5500
|
def check_rsc_supported_react_version_for_package(rsc_package, package_name, package_version)
|
|
4703
5501
|
return true if package_version.blank?
|
|
4704
|
-
|
|
4705
|
-
rsc_minor = npm_version_tuple(rsc_package["version"])[1]
|
|
4706
|
-
minimum_version = RSC_REACT_MINIMUM_BY_PACKAGE_MINOR.fetch(rsc_minor)
|
|
4707
|
-
return true if npm_prerelease(package_version).blank? && supported_rsc_react_line?(package_version) &&
|
|
4708
|
-
!npm_version_less_than?(package_version, minimum_version)
|
|
5502
|
+
return true if supported_rsc_react_version_for_package?(package_version, rsc_package["version"].to_s)
|
|
4709
5503
|
|
|
4710
5504
|
package_label = package_name == "react" ? "React" : "React DOM"
|
|
5505
|
+
rsc_version = rsc_package["version"].to_s
|
|
5506
|
+
react_range_label = supported_react_range_label_for_rsc_package(rsc_version)
|
|
5507
|
+
react_install = recommended_react_install_version_for_rsc_package(rsc_version)
|
|
4711
5508
|
|
|
4712
5509
|
checker.add_error(<<~MSG.strip)
|
|
4713
5510
|
🚫 #{RSC_PACKAGE_NAME} #{rsc_package['version']} is installed with unsupported #{package_label} #{package_version}.
|
|
4714
5511
|
|
|
4715
|
-
React on Rails Pro 17 RSC currently supports
|
|
5512
|
+
React on Rails Pro 17 RSC currently supports React/React DOM #{react_range_label}.
|
|
4716
5513
|
The node renderer enforces the same support window at startup.
|
|
4717
5514
|
|
|
4718
|
-
Fix: npm install react@~#{
|
|
5515
|
+
Fix: npm install react@~#{react_install} react-dom@~#{react_install} --save-exact
|
|
4719
5516
|
MSG
|
|
4720
5517
|
false
|
|
4721
5518
|
end
|
|
4722
5519
|
|
|
5520
|
+
def supported_rsc_react_version_for_package?(react_version, rsc_version)
|
|
5521
|
+
range = rsc_react_support_range_for(rsc_version)
|
|
5522
|
+
return false unless range
|
|
5523
|
+
# The prerelease exception is for the RSC package soak, not its React peers.
|
|
5524
|
+
return false if npm_prerelease(react_version).present?
|
|
5525
|
+
|
|
5526
|
+
major, minor, patch = npm_version_tuple(react_version)
|
|
5527
|
+
major == RSC_SUPPORTED_REACT_MAJOR &&
|
|
5528
|
+
minor == range.fetch(:minor) &&
|
|
5529
|
+
patch >= range.fetch(:min_patch)
|
|
5530
|
+
end
|
|
5531
|
+
|
|
5532
|
+
def supported_react_range_label_for_rsc_package(rsc_version)
|
|
5533
|
+
range = rsc_react_support_range_for(rsc_version)
|
|
5534
|
+
return RSC_SUPPORTED_REACT_LINE unless range
|
|
5535
|
+
|
|
5536
|
+
"#{RSC_SUPPORTED_REACT_MAJOR}.#{range.fetch(:minor)}.x with patch >= " \
|
|
5537
|
+
"#{RSC_SUPPORTED_REACT_MAJOR}.#{range.fetch(:minor)}.#{range.fetch(:min_patch)} (stable releases only)"
|
|
5538
|
+
end
|
|
5539
|
+
|
|
5540
|
+
def recommended_react_install_version_for_rsc_package(rsc_version)
|
|
5541
|
+
range = rsc_react_support_range_for(rsc_version)
|
|
5542
|
+
return RSC_MINIMUM_REACT_VERSION unless range
|
|
5543
|
+
|
|
5544
|
+
"#{RSC_SUPPORTED_REACT_MAJOR}.#{range.fetch(:minor)}.#{range.fetch(:min_patch)}"
|
|
5545
|
+
end
|
|
5546
|
+
|
|
4723
5547
|
def check_rsc_react_dom_matches_react_for_package(rsc_package, react_version, react_dom_version)
|
|
4724
5548
|
return true if react_dom_version.blank? || react_dom_version == react_version
|
|
4725
5549
|
|
|
@@ -4802,11 +5626,19 @@ module ReactOnRails
|
|
|
4802
5626
|
React Server Components depend on React internal server APIs that can change between React minors.
|
|
4803
5627
|
|
|
4804
5628
|
Fix: install matching versions, for example:
|
|
4805
|
-
npm install #{package_name}@#{peer_range} #{RSC_PACKAGE_NAME}@#{rsc_version} --save-exact
|
|
5629
|
+
npm install #{package_name}@#{rsc_peer_install_spec(rsc_version, peer_range)} #{RSC_PACKAGE_NAME}@#{rsc_version} --save-exact
|
|
4806
5630
|
MSG
|
|
4807
5631
|
false
|
|
4808
5632
|
end
|
|
4809
5633
|
|
|
5634
|
+
# A package peer range such as ^19.2.8 can admit React minors the support table rejects for that RSC
|
|
5635
|
+
# release, so prefer the table's React line when the installed RSC version has one.
|
|
5636
|
+
def rsc_peer_install_spec(rsc_version, peer_range)
|
|
5637
|
+
return peer_range unless rsc_react_support_range_for(rsc_version)
|
|
5638
|
+
|
|
5639
|
+
"~#{recommended_react_install_version_for_rsc_package(rsc_version)}"
|
|
5640
|
+
end
|
|
5641
|
+
|
|
4810
5642
|
def check_rsc_package_dist_tags(rsc_package, package_root)
|
|
4811
5643
|
if ENV[SKIP_NPM_DIST_TAG_CHECK_ENV].present?
|
|
4812
5644
|
checker.add_info(
|
|
@@ -4825,8 +5657,7 @@ module ReactOnRails
|
|
|
4825
5657
|
checker.add_warning(<<~MSG.strip)
|
|
4826
5658
|
⚠️ #{RSC_PACKAGE_NAME} #{installed_version} is behind the npm #{tag} dist-tag #{tag_version}.
|
|
4827
5659
|
|
|
4828
|
-
|
|
4829
|
-
and the React runtime versions supported by this React on Rails Pro release.
|
|
5660
|
+
React Server Components track React minor versions. If your React version is on the #{tag_version.split('.')[0, 2].join('.')} line, install the matching RSC package instead of relying on a stale latest tag.
|
|
4830
5661
|
|
|
4831
5662
|
Check peer requirements with:
|
|
4832
5663
|
npm view #{RSC_PACKAGE_NAME}@#{tag_version} peerDependencies
|