react_on_rails 16.3.0 → 16.4.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.lock +1 -1
- data/Steepfile +1 -0
- data/lib/generators/react_on_rails/dev_tests_generator.rb +10 -3
- data/lib/generators/react_on_rails/install_generator.rb +15 -11
- data/lib/generators/react_on_rails/templates/base/base/bin/dev +49 -0
- data/lib/generators/react_on_rails/templates/base/base/bin/shakapacker-precompile-hook +5 -0
- data/lib/generators/react_on_rails/templates/base/base/config/initializers/react_on_rails.rb.tt +3 -3
- data/lib/react_on_rails/configuration.rb +14 -4
- data/lib/react_on_rails/dev/database_checker.rb +168 -0
- data/lib/react_on_rails/dev/server_manager.rb +72 -16
- data/lib/react_on_rails/doctor.rb +2 -2
- data/lib/react_on_rails/helper.rb +53 -2
- data/lib/react_on_rails/locales/base.rb +15 -0
- data/lib/react_on_rails/packer_utils.rb +37 -5
- data/lib/react_on_rails/packs_generator.rb +141 -11
- data/lib/react_on_rails/smart_error.rb +26 -0
- data/lib/react_on_rails/test_helper.rb +1 -1
- data/lib/react_on_rails/version.rb +1 -1
- data/lib/react_on_rails/version_checker.rb +11 -3
- data/lib/tasks/generate_packs.rake +7 -3
- data/rakelib/dummy_apps.rake +3 -3
- data/rakelib/node_package.rake +3 -3
- data/rakelib/rbs.rake +1 -1
- data/rakelib/shakapacker_examples.rake +14 -6
- data/sig/react_on_rails/configuration.rbs +2 -0
- data/sig/react_on_rails/dev/database_checker.rbs +27 -0
- data/sig/react_on_rails/packer_utils.rbs +5 -1
- metadata +4 -2
|
@@ -4,6 +4,7 @@ require "English"
|
|
|
4
4
|
require "open3"
|
|
5
5
|
require "rainbow"
|
|
6
6
|
require_relative "../packer_utils"
|
|
7
|
+
require_relative "database_checker"
|
|
7
8
|
require_relative "service_checker"
|
|
8
9
|
|
|
9
10
|
module ReactOnRails
|
|
@@ -12,16 +13,20 @@ module ReactOnRails
|
|
|
12
13
|
HELP_FLAGS = ["-h", "--help"].freeze
|
|
13
14
|
|
|
14
15
|
class << self
|
|
15
|
-
def start(mode = :development, procfile = nil, verbose: false, route: nil, rails_env: nil
|
|
16
|
+
def start(mode = :development, procfile = nil, verbose: false, route: nil, rails_env: nil,
|
|
17
|
+
skip_database_check: false)
|
|
16
18
|
case mode
|
|
17
19
|
when :production_like
|
|
18
|
-
run_production_like(_verbose: verbose, route: route, rails_env: rails_env
|
|
20
|
+
run_production_like(_verbose: verbose, route: route, rails_env: rails_env,
|
|
21
|
+
skip_database_check: skip_database_check)
|
|
19
22
|
when :static
|
|
20
23
|
procfile ||= "Procfile.dev-static-assets"
|
|
21
|
-
run_static_development(procfile, verbose: verbose, route: route
|
|
24
|
+
run_static_development(procfile, verbose: verbose, route: route,
|
|
25
|
+
skip_database_check: skip_database_check)
|
|
22
26
|
when :development, :hmr
|
|
23
27
|
procfile ||= "Procfile.dev"
|
|
24
|
-
run_development(procfile, verbose: verbose, route: route
|
|
28
|
+
run_development(procfile, verbose: verbose, route: route,
|
|
29
|
+
skip_database_check: skip_database_check)
|
|
25
30
|
else
|
|
26
31
|
raise ArgumentError, "Unknown mode: #{mode}"
|
|
27
32
|
end
|
|
@@ -151,7 +156,7 @@ module ReactOnRails
|
|
|
151
156
|
# Flags that take a value as the next argument (not using = syntax)
|
|
152
157
|
FLAGS_WITH_VALUES = %w[--route --rails-env].freeze
|
|
153
158
|
|
|
154
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
159
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
155
160
|
def run_from_command_line(args = ARGV)
|
|
156
161
|
require "optparse"
|
|
157
162
|
|
|
@@ -163,7 +168,7 @@ module ReactOnRails
|
|
|
163
168
|
# Check if help flags are present in args (before OptionParser processes them)
|
|
164
169
|
help_requested = args.any? { |arg| HELP_FLAGS.include?(arg) }
|
|
165
170
|
|
|
166
|
-
options = { route: nil, rails_env: nil, verbose: false }
|
|
171
|
+
options = { route: nil, rails_env: nil, verbose: false, skip_database_check: false }
|
|
167
172
|
|
|
168
173
|
OptionParser.new do |opts|
|
|
169
174
|
opts.banner = "Usage: dev [command] [options]"
|
|
@@ -180,6 +185,10 @@ module ReactOnRails
|
|
|
180
185
|
options[:verbose] = true
|
|
181
186
|
end
|
|
182
187
|
|
|
188
|
+
opts.on("--skip-database-check", "Skip database connectivity check (saves ~1-2s startup time)") do
|
|
189
|
+
options[:skip_database_check] = true
|
|
190
|
+
end
|
|
191
|
+
|
|
183
192
|
opts.on("-h", "--help", "Prints this help") do
|
|
184
193
|
show_help
|
|
185
194
|
exit
|
|
@@ -200,22 +209,25 @@ module ReactOnRails
|
|
|
200
209
|
case command
|
|
201
210
|
when "production-assets", "prod"
|
|
202
211
|
start(:production_like, nil, verbose: options[:verbose], route: options[:route],
|
|
203
|
-
rails_env: options[:rails_env]
|
|
212
|
+
rails_env: options[:rails_env],
|
|
213
|
+
skip_database_check: options[:skip_database_check])
|
|
204
214
|
when "static"
|
|
205
|
-
start(:static, "Procfile.dev-static-assets", verbose: options[:verbose], route: options[:route]
|
|
215
|
+
start(:static, "Procfile.dev-static-assets", verbose: options[:verbose], route: options[:route],
|
|
216
|
+
skip_database_check: options[:skip_database_check])
|
|
206
217
|
when "kill"
|
|
207
218
|
kill_processes
|
|
208
219
|
when "help"
|
|
209
220
|
show_help
|
|
210
221
|
when "hmr", nil
|
|
211
|
-
start(:development, "Procfile.dev", verbose: options[:verbose], route: options[:route]
|
|
222
|
+
start(:development, "Procfile.dev", verbose: options[:verbose], route: options[:route],
|
|
223
|
+
skip_database_check: options[:skip_database_check])
|
|
212
224
|
else
|
|
213
225
|
puts "Unknown argument: #{command}"
|
|
214
226
|
puts "Run 'dev help' for usage information"
|
|
215
227
|
exit 1
|
|
216
228
|
end
|
|
217
229
|
end
|
|
218
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
230
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
219
231
|
|
|
220
232
|
private
|
|
221
233
|
|
|
@@ -300,13 +312,35 @@ module ReactOnRails
|
|
|
300
312
|
def warn_if_shakapacker_version_too_old
|
|
301
313
|
# Only warn for Shakapacker versions in the range 9.0.0 to 9.3.x
|
|
302
314
|
# Versions below 9.0.0 don't use the precompile_hook feature
|
|
303
|
-
# Versions 9.4.0+ support SHAKAPACKER_SKIP_PRECOMPILE_HOOK environment variable
|
|
315
|
+
# Versions 9.4.0+ support SHAKAPACKER_SKIP_PRECOMPILE_HOOK environment variable natively
|
|
304
316
|
has_precompile_hook_support = PackerUtils.shakapacker_version_requirement_met?("9.0.0")
|
|
305
317
|
has_skip_env_var_support = PackerUtils.shakapacker_version_requirement_met?("9.4.0")
|
|
306
318
|
|
|
307
319
|
return unless has_precompile_hook_support
|
|
308
320
|
return if has_skip_env_var_support
|
|
309
321
|
|
|
322
|
+
hook_value = PackerUtils.shakapacker_precompile_hook_value
|
|
323
|
+
return unless hook_value
|
|
324
|
+
|
|
325
|
+
# Case 1: Script-based hook WITH self-guard -> fully protected, no warning needed
|
|
326
|
+
return if PackerUtils.hook_script_has_self_guard?(hook_value)
|
|
327
|
+
|
|
328
|
+
# Case 2: Script-based hook WITHOUT self-guard -> actionable warning
|
|
329
|
+
script_path = PackerUtils.resolve_hook_script_path(hook_value)
|
|
330
|
+
if script_path
|
|
331
|
+
puts ""
|
|
332
|
+
puts Rainbow("⚠️ Warning: #{script_path} is missing the self-guard line").yellow.bold
|
|
333
|
+
puts ""
|
|
334
|
+
puts Rainbow(" Without it, the precompile hook may run multiple times in HMR mode").yellow
|
|
335
|
+
puts Rainbow(" (once by bin/dev, and again by each webpack process).").yellow
|
|
336
|
+
puts ""
|
|
337
|
+
puts Rainbow(" Add this line near the top of your hook script:").cyan
|
|
338
|
+
puts Rainbow(' exit 0 if ENV["SHAKAPACKER_SKIP_PRECOMPILE_HOOK"] == "true"').cyan.bold
|
|
339
|
+
puts ""
|
|
340
|
+
return
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Case 3: Direct command hook -> suggest upgrade or switch to script-based hook
|
|
310
344
|
puts ""
|
|
311
345
|
puts Rainbow("⚠️ Warning: Shakapacker #{PackerUtils.shakapacker_version} detected").yellow.bold
|
|
312
346
|
puts ""
|
|
@@ -315,8 +349,11 @@ module ReactOnRails
|
|
|
315
349
|
puts Rainbow(" precompile_hook to run multiple times (once by bin/dev, and again").yellow
|
|
316
350
|
puts Rainbow(" by each webpack process).").yellow
|
|
317
351
|
puts ""
|
|
318
|
-
puts Rainbow("
|
|
319
|
-
puts Rainbow("
|
|
352
|
+
puts Rainbow(" Recommendations:").cyan
|
|
353
|
+
puts Rainbow(" 1. Upgrade to Shakapacker 9.4.0 or later:").cyan
|
|
354
|
+
puts Rainbow(" bundle update shakapacker").cyan.bold
|
|
355
|
+
puts Rainbow(" 2. Or switch to a script-based hook with a self-guard.").cyan
|
|
356
|
+
puts Rainbow(" See: https://www.shakacode.com/react-on-rails/docs/building-features/process-managers").cyan
|
|
320
357
|
puts ""
|
|
321
358
|
end
|
|
322
359
|
# rubocop:enable Metrics/AbcSize
|
|
@@ -352,11 +389,13 @@ module ReactOnRails
|
|
|
352
389
|
#{Rainbow('--route ROUTE').green.bold} #{Rainbow('Specify route to display in URLs (default: root)').white}
|
|
353
390
|
#{Rainbow('--rails-env ENV').green.bold} #{Rainbow('Override RAILS_ENV for assets:precompile step only (prod mode only)').white}
|
|
354
391
|
#{Rainbow('--verbose, -v').green.bold} #{Rainbow('Enable verbose output for pack generation').white}
|
|
392
|
+
#{Rainbow('--skip-database-check').green.bold} #{Rainbow('Skip database connectivity check (saves ~1-2s startup time)').white}
|
|
355
393
|
|
|
356
394
|
#{Rainbow('📝 EXAMPLES:').cyan.bold}
|
|
357
395
|
#{Rainbow('bin/dev prod').green.bold} #{Rainbow('# NODE_ENV=production, RAILS_ENV=development').white}
|
|
358
396
|
#{Rainbow('bin/dev prod --rails-env=production').green.bold} #{Rainbow('# NODE_ENV=production, RAILS_ENV=production').white}
|
|
359
397
|
#{Rainbow('bin/dev prod --route=dashboard').green.bold} #{Rainbow('# Custom route in URLs').white}
|
|
398
|
+
#{Rainbow('bin/dev --skip-database-check').green.bold} #{Rainbow('# Skip DB check for faster startup').white}
|
|
360
399
|
OPTIONS
|
|
361
400
|
end
|
|
362
401
|
# rubocop:enable Metrics/AbcSize
|
|
@@ -373,6 +412,14 @@ module ReactOnRails
|
|
|
373
412
|
|
|
374
413
|
#{Rainbow('Edit these files to customize the development environment for your needs.').white}
|
|
375
414
|
|
|
415
|
+
#{Rainbow('🗄️ DATABASE CHECK:').cyan.bold}
|
|
416
|
+
#{Rainbow('bin/dev checks database connectivity before starting (adds ~1-2s to startup).').white}
|
|
417
|
+
#{Rainbow('Disable this check if you don\'t use a database or want faster startup:').white}
|
|
418
|
+
|
|
419
|
+
#{Rainbow('•').yellow} #{Rainbow('CLI flag:').white} #{Rainbow('bin/dev --skip-database-check').green.bold}
|
|
420
|
+
#{Rainbow('•').yellow} #{Rainbow('Environment:').white} #{Rainbow('SKIP_DATABASE_CHECK=true bin/dev').green.bold}
|
|
421
|
+
#{Rainbow('•').yellow} #{Rainbow('Config:').white} #{Rainbow('config.check_database_on_dev_start = false').green.bold} #{Rainbow('(in react_on_rails.rb)').white}
|
|
422
|
+
|
|
376
423
|
#{Rainbow('🔍 SERVICE DEPENDENCIES:').cyan.bold}
|
|
377
424
|
#{Rainbow('Configure required external services in').white} #{Rainbow('.dev-services.yml').green.bold}#{Rainbow(':').white}
|
|
378
425
|
|
|
@@ -426,7 +473,7 @@ module ReactOnRails
|
|
|
426
473
|
# rubocop:enable Metrics/AbcSize
|
|
427
474
|
|
|
428
475
|
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
429
|
-
def run_production_like(_verbose: false, route: nil, rails_env: nil)
|
|
476
|
+
def run_production_like(_verbose: false, route: nil, rails_env: nil, skip_database_check: false)
|
|
430
477
|
procfile = "Procfile.dev-prod-assets"
|
|
431
478
|
|
|
432
479
|
features = [
|
|
@@ -441,6 +488,9 @@ module ReactOnRails
|
|
|
441
488
|
|
|
442
489
|
print_procfile_info(procfile, route: route)
|
|
443
490
|
|
|
491
|
+
# Check database setup before starting
|
|
492
|
+
exit 1 unless DatabaseChecker.check_database(skip: skip_database_check)
|
|
493
|
+
|
|
444
494
|
# Check required services before starting
|
|
445
495
|
exit 1 unless ServiceChecker.check_services
|
|
446
496
|
|
|
@@ -563,9 +613,12 @@ module ReactOnRails
|
|
|
563
613
|
end
|
|
564
614
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
565
615
|
|
|
566
|
-
def run_static_development(procfile, verbose: false, route: nil)
|
|
616
|
+
def run_static_development(procfile, verbose: false, route: nil, skip_database_check: false)
|
|
567
617
|
print_procfile_info(procfile, route: route)
|
|
568
618
|
|
|
619
|
+
# Check database setup before starting
|
|
620
|
+
exit 1 unless DatabaseChecker.check_database(skip: skip_database_check)
|
|
621
|
+
|
|
569
622
|
# Check required services before starting
|
|
570
623
|
exit 1 unless ServiceChecker.check_services
|
|
571
624
|
|
|
@@ -592,9 +645,12 @@ module ReactOnRails
|
|
|
592
645
|
ProcessManager.run_with_process_manager(procfile)
|
|
593
646
|
end
|
|
594
647
|
|
|
595
|
-
def run_development(procfile, verbose: false, route: nil)
|
|
648
|
+
def run_development(procfile, verbose: false, route: nil, skip_database_check: false)
|
|
596
649
|
print_procfile_info(procfile, route: route)
|
|
597
650
|
|
|
651
|
+
# Check database setup before starting
|
|
652
|
+
exit 1 unless DatabaseChecker.check_database(skip: skip_database_check)
|
|
653
|
+
|
|
598
654
|
# Check required services before starting
|
|
599
655
|
exit 1 unless ServiceChecker.check_services
|
|
600
656
|
|
|
@@ -1193,7 +1193,7 @@ module ReactOnRails
|
|
|
1193
1193
|
checker.add_info(" 💡 These are mutually exclusive - use only one approach")
|
|
1194
1194
|
checker.add_info(" 💡 Recommended: Use compile: true in shakapacker.yml (simpler)")
|
|
1195
1195
|
checker.add_info(" 💡 Alternative: Use build_test_command with ReactOnRails::TestHelper (explicit control)")
|
|
1196
|
-
checker.add_info(" 📖 See: https://github.com/shakacode/react_on_rails/blob/master/docs/
|
|
1196
|
+
checker.add_info(" 📖 See: https://github.com/shakacode/react_on_rails/blob/master/docs/building-features/testing-configuration.md")
|
|
1197
1197
|
elsif has_build_test_command && !uses_test_helper
|
|
1198
1198
|
checker.add_warning(" ⚠️ build_test_command is set but ReactOnRails::TestHelper is not configured")
|
|
1199
1199
|
checker.add_info(" 💡 Add to spec/rails_helper.rb:")
|
|
@@ -1208,7 +1208,7 @@ module ReactOnRails
|
|
|
1208
1208
|
checker.add_warning(" ⚠️ No test asset compilation configured")
|
|
1209
1209
|
checker.add_info(" 💡 Recommended: Add to shakapacker.yml test section:")
|
|
1210
1210
|
checker.add_info(" compile: true")
|
|
1211
|
-
checker.add_info(" 📖 See: https://github.com/shakacode/react_on_rails/blob/master/docs/
|
|
1211
|
+
checker.add_info(" 📖 See: https://github.com/shakacode/react_on_rails/blob/master/docs/building-features/testing-configuration.md")
|
|
1212
1212
|
elsif has_compile_true
|
|
1213
1213
|
checker.add_success(" ✅ Test assets configured via Shakapacker auto-compilation")
|
|
1214
1214
|
checker.add_info(" (compile: true in shakapacker.yml)")
|
|
@@ -151,15 +151,26 @@ module ReactOnRails
|
|
|
151
151
|
# Instead, you should use the standard react_component view helper.
|
|
152
152
|
#
|
|
153
153
|
# store_name: name of the store, corresponding to your call to ReactOnRails.registerStores in your
|
|
154
|
-
# JavaScript code.
|
|
154
|
+
# JavaScript code. When using auto-bundling, this should match the filename of your
|
|
155
|
+
# store file (e.g., "commentsStore" for commentsStore.js).
|
|
155
156
|
# props: Ruby Hash or JSON string which contains the properties to pass to the redux store.
|
|
156
157
|
# Options
|
|
157
158
|
# defer: false -- pass as true if you wish to render this below your component.
|
|
158
159
|
# immediate_hydration: nil -- React on Rails Pro (licensed) feature. When nil (default), Pro users
|
|
159
160
|
# get immediate hydration, non-Pro users don't. Can be explicitly overridden.
|
|
160
|
-
|
|
161
|
+
# auto_load_bundle: nil -- If true, automatically loads the generated pack for this store.
|
|
162
|
+
# Defaults to ReactOnRails.configuration.auto_load_bundle if not specified.
|
|
163
|
+
# Requires config.stores_subdirectory to be set (e.g., "ror_stores").
|
|
164
|
+
# Store files should be placed in directories matching this name, e.g.:
|
|
165
|
+
# app/javascript/bundles/ror_stores/commentsStore.js
|
|
166
|
+
# The store file must export default a store generator function.
|
|
167
|
+
def redux_store(store_name, props: {}, defer: false, immediate_hydration: nil, auto_load_bundle: nil)
|
|
161
168
|
immediate_hydration = ReactOnRails::Utils.normalize_immediate_hydration(immediate_hydration, store_name, "Store")
|
|
162
169
|
|
|
170
|
+
# Auto-load store pack if configured
|
|
171
|
+
should_auto_load = auto_load_bundle.nil? ? ReactOnRails.configuration.auto_load_bundle : auto_load_bundle
|
|
172
|
+
load_pack_for_generated_store(store_name, explicit_auto_load: auto_load_bundle == true) if should_auto_load
|
|
173
|
+
|
|
163
174
|
redux_store_data = { store_name: store_name,
|
|
164
175
|
props: props,
|
|
165
176
|
immediate_hydration: immediate_hydration }
|
|
@@ -348,6 +359,34 @@ module ReactOnRails
|
|
|
348
359
|
append_stylesheet_pack_tag("generated/#{react_component_name}")
|
|
349
360
|
end
|
|
350
361
|
|
|
362
|
+
def load_pack_for_generated_store(store_name, explicit_auto_load: false)
|
|
363
|
+
unless ReactOnRails.configuration.stores_subdirectory.present?
|
|
364
|
+
if explicit_auto_load
|
|
365
|
+
raise ReactOnRails::SmartError.new(
|
|
366
|
+
error_type: :configuration_error,
|
|
367
|
+
details: "auto_load_bundle is enabled for store " \
|
|
368
|
+
"'#{store_name}', but " \
|
|
369
|
+
"stores_subdirectory is not configured. " \
|
|
370
|
+
"Set config.stores_subdirectory (e.g., " \
|
|
371
|
+
"'ror_stores') in your ReactOnRails " \
|
|
372
|
+
"configuration so that store packs can " \
|
|
373
|
+
"be generated and loaded."
|
|
374
|
+
)
|
|
375
|
+
end
|
|
376
|
+
return
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
ReactOnRails::PackerUtils.raise_nested_entries_disabled unless ReactOnRails::PackerUtils.nested_entries?
|
|
380
|
+
if Rails.env.development?
|
|
381
|
+
is_store_pack_present = File.exist?(generated_stores_pack_path(store_name))
|
|
382
|
+
raise_missing_autoloaded_store_bundle(store_name) unless is_store_pack_present
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
options = { defer: ReactOnRails.configuration.generated_component_packs_loading_strategy == :defer }
|
|
386
|
+
options[:async] = true if ReactOnRails.configuration.generated_component_packs_loading_strategy == :async
|
|
387
|
+
append_javascript_pack_tag("generated/#{store_name}", **options)
|
|
388
|
+
end
|
|
389
|
+
|
|
351
390
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
352
391
|
|
|
353
392
|
def registered_stores
|
|
@@ -376,6 +415,10 @@ module ReactOnRails
|
|
|
376
415
|
"#{ReactOnRails::PackerUtils.packer_source_entry_path}/generated/#{component_name}.js"
|
|
377
416
|
end
|
|
378
417
|
|
|
418
|
+
def generated_stores_pack_path(store_name)
|
|
419
|
+
"#{ReactOnRails::PackerUtils.packer_source_entry_path}/generated/#{store_name}.js"
|
|
420
|
+
end
|
|
421
|
+
|
|
379
422
|
def build_react_component_result_for_server_rendered_string(
|
|
380
423
|
server_rendered_html: required("server_rendered_html"),
|
|
381
424
|
component_specification_tag: required("component_specification_tag"),
|
|
@@ -681,6 +724,14 @@ module ReactOnRails
|
|
|
681
724
|
expected_path: generated_components_pack_path(react_component_name)
|
|
682
725
|
)
|
|
683
726
|
end
|
|
727
|
+
|
|
728
|
+
def raise_missing_autoloaded_store_bundle(store_name)
|
|
729
|
+
raise ReactOnRails::SmartError.new(
|
|
730
|
+
error_type: :missing_auto_loaded_store_bundle,
|
|
731
|
+
component_name: store_name,
|
|
732
|
+
expected_path: generated_stores_pack_path(store_name)
|
|
733
|
+
)
|
|
734
|
+
end
|
|
684
735
|
end
|
|
685
736
|
end
|
|
686
737
|
# rubocop:enable Metrics/ModuleLength
|
|
@@ -4,6 +4,21 @@ require "erb"
|
|
|
4
4
|
|
|
5
5
|
module ReactOnRails
|
|
6
6
|
module Locales
|
|
7
|
+
# Compiles locale YAML files into JavaScript or JSON for use with React on Rails i18n.
|
|
8
|
+
#
|
|
9
|
+
# Reads YAML locale files from +config.i18n_yml_dir+ (or Rails i18n load path),
|
|
10
|
+
# generates output files in +config.i18n_dir+, and skips generation when output
|
|
11
|
+
# files are already up-to-date (unless +force+ is true).
|
|
12
|
+
#
|
|
13
|
+
# @param force [Boolean] when true, regenerate even if output files are current
|
|
14
|
+
# @return [ReactOnRails::Locales::ToJs, ReactOnRails::Locales::ToJson] the converter instance
|
|
15
|
+
# @raise [ReactOnRails::Error] if configured directories do not exist
|
|
16
|
+
#
|
|
17
|
+
# @example Basic usage (skips if up-to-date)
|
|
18
|
+
# ReactOnRails::Locales.compile
|
|
19
|
+
#
|
|
20
|
+
# @example Force regeneration
|
|
21
|
+
# ReactOnRails::Locales.compile(force: true)
|
|
7
22
|
def self.compile(force: false)
|
|
8
23
|
config = ReactOnRails.configuration
|
|
9
24
|
check_config_directory_exists(
|
|
@@ -179,15 +179,32 @@ module ReactOnRails
|
|
|
179
179
|
end
|
|
180
180
|
|
|
181
181
|
def self.extract_precompile_hook
|
|
182
|
-
#
|
|
183
|
-
|
|
182
|
+
# Prefer the public API (available in Shakapacker 9.0+)
|
|
183
|
+
return ::Shakapacker.config.precompile_hook if ::Shakapacker.config.respond_to?(:precompile_hook)
|
|
184
|
+
|
|
185
|
+
# Fallback: access config data using private :data method
|
|
184
186
|
config_data = ::Shakapacker.config.send(:data)
|
|
185
187
|
|
|
186
188
|
# Try symbol keys first (Shakapacker's internal format), then fall back to string keys
|
|
187
|
-
# The key is 'precompile_hook' at the top level of the config
|
|
188
189
|
config_data&.[](:precompile_hook) || config_data&.[]("precompile_hook")
|
|
189
190
|
end
|
|
190
191
|
|
|
192
|
+
# Regex pattern to detect pack generation in hook scripts
|
|
193
|
+
# Matches both:
|
|
194
|
+
# - The rake task: react_on_rails:generate_packs
|
|
195
|
+
# - The Ruby method: generate_packs_if_stale (used by generator template)
|
|
196
|
+
GENERATE_PACKS_PATTERN = /\b(react_on_rails:generate_packs|generate_packs_if_stale)\b/
|
|
197
|
+
|
|
198
|
+
# Pattern to detect a real self-guard statement that exits early when
|
|
199
|
+
# SHAKAPACKER_SKIP_PRECOMPILE_HOOK is true. This avoids false positives
|
|
200
|
+
# from comments or unrelated string literals.
|
|
201
|
+
SELF_GUARD_PATTERN = /
|
|
202
|
+
(?:^|\s)
|
|
203
|
+
(?:exit|return)
|
|
204
|
+
(?:\s+0)?
|
|
205
|
+
\s+if\s+ENV\[(["'])SHAKAPACKER_SKIP_PRECOMPILE_HOOK\1\]\s*==\s*(["'])true\2
|
|
206
|
+
/x
|
|
207
|
+
|
|
191
208
|
def self.hook_contains_generate_packs?(hook_value)
|
|
192
209
|
# The hook value can be either:
|
|
193
210
|
# 1. A direct command containing the rake task
|
|
@@ -195,7 +212,7 @@ module ReactOnRails
|
|
|
195
212
|
return false if hook_value.blank?
|
|
196
213
|
|
|
197
214
|
# Check if it's a direct command first
|
|
198
|
-
return true if hook_value.to_s.match?(
|
|
215
|
+
return true if hook_value.to_s.match?(GENERATE_PACKS_PATTERN)
|
|
199
216
|
|
|
200
217
|
# Check if it's a script file path
|
|
201
218
|
script_path = resolve_hook_script_path(hook_value)
|
|
@@ -203,7 +220,7 @@ module ReactOnRails
|
|
|
203
220
|
|
|
204
221
|
# Read and check script contents
|
|
205
222
|
script_contents = File.read(script_path)
|
|
206
|
-
script_contents.match?(
|
|
223
|
+
script_contents.match?(GENERATE_PACKS_PATTERN)
|
|
207
224
|
rescue StandardError
|
|
208
225
|
# If we can't read the script, assume it doesn't contain generate_packs
|
|
209
226
|
false
|
|
@@ -217,6 +234,21 @@ module ReactOnRails
|
|
|
217
234
|
potential_path if potential_path.file?
|
|
218
235
|
end
|
|
219
236
|
|
|
237
|
+
# Check if a hook script file contains the self-guard pattern that prevents
|
|
238
|
+
# duplicate execution when SHAKAPACKER_SKIP_PRECOMPILE_HOOK is set.
|
|
239
|
+
# Returns false for direct command hooks (non-script values).
|
|
240
|
+
def self.hook_script_has_self_guard?(hook_value)
|
|
241
|
+
return false if hook_value.blank?
|
|
242
|
+
|
|
243
|
+
script_path = resolve_hook_script_path(hook_value)
|
|
244
|
+
return false unless script_path
|
|
245
|
+
|
|
246
|
+
script_contents = File.read(script_path)
|
|
247
|
+
script_contents.match?(SELF_GUARD_PATTERN)
|
|
248
|
+
rescue StandardError
|
|
249
|
+
false
|
|
250
|
+
end
|
|
251
|
+
|
|
220
252
|
# Returns the configured precompile hook value for logging/debugging
|
|
221
253
|
# Returns nil if no hook is configured
|
|
222
254
|
def self.shakapacker_precompile_hook_value
|
|
@@ -48,12 +48,34 @@ module ReactOnRails
|
|
|
48
48
|
private
|
|
49
49
|
|
|
50
50
|
def generate_packs(verbose: false)
|
|
51
|
+
# Check for name conflicts between components and stores
|
|
52
|
+
check_for_component_store_name_conflicts
|
|
53
|
+
|
|
51
54
|
common_component_to_path.each_value { |component_path| create_pack(component_path, verbose: verbose) }
|
|
52
55
|
client_component_to_path.each_value { |component_path| create_pack(component_path, verbose: verbose) }
|
|
53
56
|
|
|
57
|
+
# Generate store packs if stores_subdirectory is configured
|
|
58
|
+
store_to_path.each_value { |store_path| create_store_pack(store_path, verbose: verbose) }
|
|
59
|
+
|
|
54
60
|
create_server_pack(verbose: verbose) if ReactOnRails.configuration.server_bundle_js_file.present?
|
|
55
61
|
end
|
|
56
62
|
|
|
63
|
+
def check_for_component_store_name_conflicts
|
|
64
|
+
component_names = common_component_to_path.keys + client_component_to_path.keys
|
|
65
|
+
store_names = store_to_path.keys
|
|
66
|
+
conflicts = component_names & store_names
|
|
67
|
+
|
|
68
|
+
return if conflicts.empty?
|
|
69
|
+
|
|
70
|
+
msg = <<~MSG
|
|
71
|
+
**ERROR** ReactOnRails: The following names are used for both components and stores: #{conflicts.join(', ')}.
|
|
72
|
+
This would cause pack file conflicts in the generated directory.
|
|
73
|
+
Please rename your components or stores to have unique names.
|
|
74
|
+
MSG
|
|
75
|
+
|
|
76
|
+
raise ReactOnRails::Error, msg
|
|
77
|
+
end
|
|
78
|
+
|
|
57
79
|
def create_pack(file_path, verbose: false)
|
|
58
80
|
output_path = generated_pack_path(file_path)
|
|
59
81
|
content = pack_file_contents(file_path)
|
|
@@ -128,6 +150,27 @@ module ReactOnRails
|
|
|
128
150
|
FILE_CONTENT
|
|
129
151
|
end
|
|
130
152
|
|
|
153
|
+
def create_store_pack(file_path, verbose: false)
|
|
154
|
+
output_path = generated_store_pack_path(file_path)
|
|
155
|
+
content = store_pack_file_contents(file_path)
|
|
156
|
+
|
|
157
|
+
File.write(output_path, content)
|
|
158
|
+
|
|
159
|
+
puts(Rainbow("Generated Store Pack: #{output_path}").yellow) if verbose
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def store_pack_file_contents(file_path)
|
|
163
|
+
registered_store_name = store_name(file_path)
|
|
164
|
+
relative_store_path = relative_store_path_from_generated_pack(file_path)
|
|
165
|
+
|
|
166
|
+
<<~FILE_CONTENT.strip
|
|
167
|
+
import ReactOnRails from '#{react_on_rails_npm_package}/client';
|
|
168
|
+
import #{registered_store_name} from '#{relative_store_path}';
|
|
169
|
+
|
|
170
|
+
ReactOnRails.registerStore({#{registered_store_name}});
|
|
171
|
+
FILE_CONTENT
|
|
172
|
+
end
|
|
173
|
+
|
|
131
174
|
def create_server_pack(verbose: false)
|
|
132
175
|
File.write(generated_server_bundle_file_path, generated_server_pack_file_content)
|
|
133
176
|
|
|
@@ -135,11 +178,13 @@ module ReactOnRails
|
|
|
135
178
|
puts(Rainbow("Generated Server Bundle: #{generated_server_bundle_file_path}").orange) if verbose
|
|
136
179
|
end
|
|
137
180
|
|
|
138
|
-
def build_server_pack_content(component_on_server_imports, server_components, client_components
|
|
181
|
+
def build_server_pack_content(component_on_server_imports, server_components, client_components,
|
|
182
|
+
store_imports: [], store_names: [])
|
|
183
|
+
all_imports = component_on_server_imports + store_imports
|
|
139
184
|
content = <<~FILE_CONTENT
|
|
140
185
|
import ReactOnRails from '#{react_on_rails_npm_package}';
|
|
141
186
|
|
|
142
|
-
#{
|
|
187
|
+
#{all_imports.join("\n")}\n
|
|
143
188
|
FILE_CONTENT
|
|
144
189
|
|
|
145
190
|
if server_components.any?
|
|
@@ -149,7 +194,11 @@ module ReactOnRails
|
|
|
149
194
|
FILE_CONTENT
|
|
150
195
|
end
|
|
151
196
|
|
|
152
|
-
content
|
|
197
|
+
content += "ReactOnRails.register({#{client_components.join(",\n")}});" if client_components.any?
|
|
198
|
+
|
|
199
|
+
content += "\nReactOnRails.registerStore({#{store_names.join(",\n")}});" if store_names.any?
|
|
200
|
+
|
|
201
|
+
content
|
|
153
202
|
end
|
|
154
203
|
|
|
155
204
|
def generated_server_pack_file_content
|
|
@@ -169,7 +218,15 @@ module ReactOnRails
|
|
|
169
218
|
end
|
|
170
219
|
client_components = component_for_server_registration_to_path.keys - server_components
|
|
171
220
|
|
|
172
|
-
|
|
221
|
+
# Include stores in server bundle
|
|
222
|
+
stores = store_to_path
|
|
223
|
+
store_imports = stores.map do |name, store_path|
|
|
224
|
+
"import #{name} from '#{relative_path(generated_server_bundle_file_path, store_path)}';"
|
|
225
|
+
end
|
|
226
|
+
store_names = stores.keys
|
|
227
|
+
|
|
228
|
+
build_server_pack_content(component_on_server_imports, server_components, client_components,
|
|
229
|
+
store_imports: store_imports, store_names: store_names)
|
|
173
230
|
end
|
|
174
231
|
|
|
175
232
|
def add_generated_pack_to_server_bundle
|
|
@@ -193,7 +250,10 @@ module ReactOnRails
|
|
|
193
250
|
def generated_server_bundle_file_path
|
|
194
251
|
return server_bundle_entrypoint if ReactOnRails.configuration.make_generated_server_bundle_the_entrypoint
|
|
195
252
|
|
|
196
|
-
|
|
253
|
+
entrypoint_ext = File.extname(server_bundle_entrypoint)
|
|
254
|
+
generated_interim_server_bundle_path = server_bundle_entrypoint.sub(
|
|
255
|
+
/#{Regexp.escape(entrypoint_ext)}$/, "-generated#{entrypoint_ext}"
|
|
256
|
+
)
|
|
197
257
|
generated_server_bundle_file_name = component_name(generated_interim_server_bundle_path)
|
|
198
258
|
source_entrypoint_parent = Pathname(ReactOnRails::PackerUtils.packer_source_entry_path).parent
|
|
199
259
|
generated_nonentrypoints_path = "#{source_entrypoint_parent}/generated"
|
|
@@ -220,6 +280,9 @@ module ReactOnRails
|
|
|
220
280
|
common_component_to_path.each_value { |path| expected_pack_files << generated_pack_path(path) }
|
|
221
281
|
client_component_to_path.each_value { |path| expected_pack_files << generated_pack_path(path) }
|
|
222
282
|
|
|
283
|
+
# Include store packs in expected files
|
|
284
|
+
store_to_path.each_value { |path| expected_pack_files << generated_store_pack_path(path) }
|
|
285
|
+
|
|
223
286
|
if ReactOnRails.configuration.server_bundle_js_file.present?
|
|
224
287
|
expected_server_bundle = generated_server_bundle_file_path
|
|
225
288
|
end
|
|
@@ -347,11 +410,10 @@ module ReactOnRails
|
|
|
347
410
|
end
|
|
348
411
|
|
|
349
412
|
def relative_path(from, to)
|
|
350
|
-
|
|
413
|
+
from_dir = Pathname.new(from).dirname
|
|
351
414
|
to_path = Pathname.new(to)
|
|
352
415
|
|
|
353
|
-
|
|
354
|
-
relative_path.sub("../", "")
|
|
416
|
+
to_path.relative_path_from(from_dir)
|
|
355
417
|
end
|
|
356
418
|
|
|
357
419
|
def generated_pack_path(file_path)
|
|
@@ -410,6 +472,49 @@ module ReactOnRails
|
|
|
410
472
|
"#{source_path}/**/#{ReactOnRails.configuration.components_subdirectory}"
|
|
411
473
|
end
|
|
412
474
|
|
|
475
|
+
def stores_search_path
|
|
476
|
+
return nil unless ReactOnRails.configuration.stores_subdirectory.present?
|
|
477
|
+
|
|
478
|
+
source_path = ReactOnRails::PackerUtils.packer_source_path
|
|
479
|
+
|
|
480
|
+
"#{source_path}/**/#{ReactOnRails.configuration.stores_subdirectory}"
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def store_to_path
|
|
484
|
+
return {} unless stores_search_path
|
|
485
|
+
|
|
486
|
+
store_paths = Dir.glob("#{stores_search_path}/*")
|
|
487
|
+
filtered_paths = filter_component_files(store_paths)
|
|
488
|
+
store_name_to_path(filtered_paths)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def store_name_to_path(paths)
|
|
492
|
+
result = {}
|
|
493
|
+
paths.each do |path|
|
|
494
|
+
name = store_name(path)
|
|
495
|
+
raise_duplicate_store_name(name, result[name], path) if result.key?(name)
|
|
496
|
+
result[name] = path
|
|
497
|
+
end
|
|
498
|
+
result
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def store_name(file_path)
|
|
502
|
+
basename = File.basename(file_path, File.extname(file_path))
|
|
503
|
+
basename.sub(CONTAINS_CLIENT_OR_SERVER_REGEX, "")
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def generated_store_pack_path(file_path)
|
|
507
|
+
"#{generated_packs_directory_path}/#{store_name(file_path)}.js"
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def relative_store_path_from_generated_pack(store_path)
|
|
511
|
+
store_file_pathname = Pathname.new(store_path)
|
|
512
|
+
store_generated_pack_path = generated_store_pack_path(store_path)
|
|
513
|
+
generated_pack_pathname = Pathname.new(store_generated_pack_path)
|
|
514
|
+
|
|
515
|
+
relative_path(generated_pack_pathname, store_file_pathname)
|
|
516
|
+
end
|
|
517
|
+
|
|
413
518
|
def raise_client_component_overrides_common(component_name)
|
|
414
519
|
msg = <<~MSG
|
|
415
520
|
**ERROR** ReactOnRails: client specific definition for Component '#{component_name}' overrides the \
|
|
@@ -439,14 +544,39 @@ module ReactOnRails
|
|
|
439
544
|
raise ReactOnRails::Error, msg
|
|
440
545
|
end
|
|
441
546
|
|
|
547
|
+
def raise_duplicate_store_name(name, existing_path, new_path)
|
|
548
|
+
msg = <<~MSG
|
|
549
|
+
**ERROR** ReactOnRails: Multiple store files resolve to the same name '#{name}':
|
|
550
|
+
- #{existing_path}
|
|
551
|
+
- #{new_path}
|
|
552
|
+
Rename one of the store files to have a unique base name.
|
|
553
|
+
MSG
|
|
554
|
+
|
|
555
|
+
raise ReactOnRails::Error, msg
|
|
556
|
+
end
|
|
557
|
+
|
|
442
558
|
def stale_or_missing_packs?
|
|
443
559
|
component_files = common_component_to_path.values + client_component_to_path.values
|
|
444
|
-
|
|
560
|
+
store_files = store_to_path.values
|
|
561
|
+
all_source_files = component_files + store_files
|
|
562
|
+
|
|
563
|
+
return false if all_source_files.empty?
|
|
564
|
+
|
|
565
|
+
most_recent_mtime = Utils.find_most_recent_mtime(all_source_files).to_i
|
|
445
566
|
|
|
446
|
-
|
|
567
|
+
# Check component packs
|
|
568
|
+
component_files.each do |file|
|
|
447
569
|
path = generated_pack_path(file)
|
|
448
|
-
!File.exist?(path) || File.mtime(path).to_i < most_recent_mtime
|
|
570
|
+
return true if !File.exist?(path) || File.mtime(path).to_i < most_recent_mtime
|
|
449
571
|
end
|
|
572
|
+
|
|
573
|
+
# Check store packs
|
|
574
|
+
store_files.each do |file|
|
|
575
|
+
path = generated_store_pack_path(file)
|
|
576
|
+
return true if !File.exist?(path) || File.mtime(path).to_i < most_recent_mtime
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
false
|
|
450
580
|
end
|
|
451
581
|
end
|
|
452
582
|
# rubocop:enable Metrics/ClassLength
|