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.
@@ -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(" Recommendation: Upgrade to Shakapacker 9.4.0 or later:").cyan
319
- puts Rainbow(" bundle update shakapacker").cyan.bold
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/guides/testing-configuration.md")
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/guides/testing-configuration.md")
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
- def redux_store(store_name, props: {}, defer: false, immediate_hydration: nil)
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
- # Access config data using private :data method since there's no public API
183
- # to access the raw configuration hash needed for hook detection
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?(/\breact_on_rails:generate_packs\b/)
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?(/\breact_on_rails:generate_packs\b/)
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
- #{component_on_server_imports.join("\n")}\n
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 + "ReactOnRails.register({#{client_components.join(",\n")}});"
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
- build_server_pack_content(component_on_server_imports, server_components, client_components)
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
- generated_interim_server_bundle_path = server_bundle_entrypoint.sub(".js", "-generated.js")
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
- from_path = Pathname.new(from)
413
+ from_dir = Pathname.new(from).dirname
351
414
  to_path = Pathname.new(to)
352
415
 
353
- relative_path = to_path.relative_path_from(from_path)
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
- most_recent_mtime = Utils.find_most_recent_mtime(component_files).to_i
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
- component_files.each_with_object([]).any? do |file|
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