diff --git a/lib/react_on_rails/dev/pack_generator.rb b/lib/react_on_rails/dev/pack_generator.rb index 5044984050..5e14be0af5 100644 --- a/lib/react_on_rails/dev/pack_generator.rb +++ b/lib/react_on_rails/dev/pack_generator.rb @@ -38,29 +38,40 @@ def generate(verbose: false) if verbose puts "๐Ÿ“ฆ Generating React on Rails packs..." - success = run_pack_generation + success = run_pack_generation(silent: false, verbose: true) else print "๐Ÿ“ฆ Generating packs... " - success = run_pack_generation(silent: true) + success = run_pack_generation(silent: true, verbose: false) puts success ? "โœ…" : "โŒ" end return if success puts "โŒ Pack generation failed" + unless verbose + puts "" + puts "๐Ÿ’ก Run with #{Rainbow('--verbose').cyan.bold} flag for detailed output:" + puts " #{Rainbow('bin/dev --verbose').green.bold}" + end exit 1 end private - def run_pack_generation(silent: false) + def run_pack_generation(silent: false, verbose: false) + # Set environment variable for child processes to respect verbose mode + ENV["REACT_ON_RAILS_VERBOSE"] = verbose ? "true" : "false" + # If we're already inside a Bundler context AND Rails is available (e.g., called from bin/dev), # we can directly require and run the task. Otherwise, use bundle exec. if should_run_directly? run_rake_task_directly(silent: silent) else - run_via_bundle_exec(silent: silent) + run_via_bundle_exec(silent: silent, verbose: verbose) end + ensure + # Clean up environment variable + ENV.delete("REACT_ON_RAILS_VERBOSE") end def should_run_directly? @@ -140,17 +151,22 @@ def handle_rake_error(error, _silent) # rubocop:enable Style/StderrPuts, Style/GlobalStdStream end - def run_via_bundle_exec(silent: false) + def run_via_bundle_exec(silent: false, verbose: false) + # Environment variable is already set in run_pack_generation, but we make it explicit here + # for clarity and to ensure it's passed to the subprocess + env = { "REACT_ON_RAILS_VERBOSE" => verbose ? "true" : "false" } + # Need to unbundle to prevent Bundler from intercepting our bundle exec call # when already running inside a Bundler context (e.g., from bin/dev) with_unbundled_context do if silent system( + env, "bundle", "exec", "rake", "react_on_rails:generate_packs", out: File::NULL, err: File::NULL ) else - system("bundle", "exec", "rake", "react_on_rails:generate_packs") + system(env, "bundle", "exec", "rake", "react_on_rails:generate_packs") end end end diff --git a/lib/react_on_rails/packs_generator.rb b/lib/react_on_rails/packs_generator.rb index 838982eb97..9ee7b3ac51 100644 --- a/lib/react_on_rails/packs_generator.rb +++ b/lib/react_on_rails/packs_generator.rb @@ -25,40 +25,42 @@ def react_on_rails_npm_package def generate_packs_if_stale return unless ReactOnRails.configuration.auto_load_bundle + verbose = ENV["REACT_ON_RAILS_VERBOSE"] == "true" + add_generated_pack_to_server_bundle # Clean any non-generated files from directories - clean_non_generated_files_with_feedback + clean_non_generated_files_with_feedback(verbose: verbose) are_generated_files_present_and_up_to_date = Dir.exist?(generated_packs_directory_path) && File.exist?(generated_server_bundle_file_path) && !stale_or_missing_packs? if are_generated_files_present_and_up_to_date - puts Rainbow("โœ… Generated packs are up to date, no regeneration needed").green + puts Rainbow("โœ… Generated packs are up to date, no regeneration needed").green if verbose return end - clean_generated_directories_with_feedback - generate_packs + clean_generated_directories_with_feedback(verbose: verbose) + generate_packs(verbose: verbose) end private - def generate_packs - common_component_to_path.each_value { |component_path| create_pack(component_path) } - client_component_to_path.each_value { |component_path| create_pack(component_path) } + def generate_packs(verbose: false) + common_component_to_path.each_value { |component_path| create_pack(component_path, verbose: verbose) } + client_component_to_path.each_value { |component_path| create_pack(component_path, verbose: verbose) } - create_server_pack if ReactOnRails.configuration.server_bundle_js_file.present? + create_server_pack(verbose: verbose) if ReactOnRails.configuration.server_bundle_js_file.present? end - def create_pack(file_path) + def create_pack(file_path, verbose: false) output_path = generated_pack_path(file_path) content = pack_file_contents(file_path) File.write(output_path, content) - puts(Rainbow("Generated Packs: #{output_path}").yellow) + puts(Rainbow("Generated Packs: #{output_path}").yellow) if verbose end def first_js_statement_in_code(content) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity @@ -126,11 +128,11 @@ def pack_file_contents(file_path) FILE_CONTENT end - def create_server_pack + def create_server_pack(verbose: false) File.write(generated_server_bundle_file_path, generated_server_pack_file_content) add_generated_pack_to_server_bundle - puts(Rainbow("Generated Server Bundle: #{generated_server_bundle_file_path}").orange) + puts(Rainbow("Generated Server Bundle: #{generated_server_bundle_file_path}").orange) if verbose end def build_server_pack_content(component_on_server_imports, server_components, client_components) @@ -200,17 +202,17 @@ def generated_server_bundle_file_path "#{generated_nonentrypoints_path}/#{generated_server_bundle_file_name}.js" end - def clean_non_generated_files_with_feedback + def clean_non_generated_files_with_feedback(verbose: false) directories_to_clean = [generated_packs_directory_path, generated_server_bundle_directory_path].compact.uniq expected_files = build_expected_files_set - puts Rainbow("๐Ÿงน Cleaning non-generated files...").yellow + puts Rainbow("๐Ÿงน Cleaning non-generated files...").yellow if verbose total_deleted = directories_to_clean.sum do |dir_path| - clean_unexpected_files_from_directory(dir_path, expected_files) + clean_unexpected_files_from_directory(dir_path, expected_files, verbose: verbose) end - display_cleanup_summary(total_deleted) + display_cleanup_summary(total_deleted, verbose: verbose) if verbose end def build_expected_files_set @@ -225,17 +227,17 @@ def build_expected_files_set { pack_files: expected_pack_files, server_bundle: expected_server_bundle } end - def clean_unexpected_files_from_directory(dir_path, expected_files) + def clean_unexpected_files_from_directory(dir_path, expected_files, verbose: false) return 0 unless Dir.exist?(dir_path) existing_files = Dir.glob("#{dir_path}/**/*").select { |f| File.file?(f) } unexpected_files = find_unexpected_files(existing_files, dir_path, expected_files) if unexpected_files.any? - delete_unexpected_files(unexpected_files, dir_path) + delete_unexpected_files(unexpected_files, dir_path, verbose: verbose) unexpected_files.length else - puts Rainbow(" No unexpected files found in #{dir_path}").cyan + puts Rainbow(" No unexpected files found in #{dir_path}").cyan if verbose 0 end end @@ -250,15 +252,21 @@ def find_unexpected_files(existing_files, dir_path, expected_files) end end - def delete_unexpected_files(unexpected_files, dir_path) - puts Rainbow(" Deleting #{unexpected_files.length} unexpected files from #{dir_path}:").cyan - unexpected_files.each do |file| - puts Rainbow(" - #{File.basename(file)}").blue - File.delete(file) + def delete_unexpected_files(unexpected_files, dir_path, verbose: false) + if verbose + puts Rainbow(" Deleting #{unexpected_files.length} unexpected files from #{dir_path}:").cyan + unexpected_files.each do |file| + puts Rainbow(" - #{File.basename(file)}").blue + File.delete(file) + end + else + unexpected_files.each { |file| File.delete(file) } end end - def display_cleanup_summary(total_deleted) + def display_cleanup_summary(total_deleted, verbose: false) + return unless verbose + if total_deleted.positive? puts Rainbow("๐Ÿ—‘๏ธ Deleted #{total_deleted} unexpected files total").red else @@ -266,15 +274,17 @@ def display_cleanup_summary(total_deleted) end end - def clean_generated_directories_with_feedback + def clean_generated_directories_with_feedback(verbose: false) directories_to_clean = [ generated_packs_directory_path, generated_server_bundle_directory_path ].compact.uniq - puts Rainbow("๐Ÿงน Cleaning generated directories...").yellow + puts Rainbow("๐Ÿงน Cleaning generated directories...").yellow if verbose + + total_deleted = directories_to_clean.sum { |dir_path| clean_directory_with_feedback(dir_path, verbose: verbose) } - total_deleted = directories_to_clean.sum { |dir_path| clean_directory_with_feedback(dir_path) } + return unless verbose if total_deleted.positive? puts Rainbow("๐Ÿ—‘๏ธ Deleted #{total_deleted} generated files total").red @@ -283,27 +293,29 @@ def clean_generated_directories_with_feedback end end - def clean_directory_with_feedback(dir_path) - return create_directory_with_feedback(dir_path) unless Dir.exist?(dir_path) + def clean_directory_with_feedback(dir_path, verbose: false) + return create_directory_with_feedback(dir_path, verbose: verbose) unless Dir.exist?(dir_path) files = Dir.glob("#{dir_path}/**/*").select { |f| File.file?(f) } if files.any? - puts Rainbow(" Deleting #{files.length} files from #{dir_path}:").cyan - files.each { |file| puts Rainbow(" - #{File.basename(file)}").blue } + if verbose + puts Rainbow(" Deleting #{files.length} files from #{dir_path}:").cyan + files.each { |file| puts Rainbow(" - #{File.basename(file)}").blue } + end FileUtils.rm_rf(dir_path) FileUtils.mkdir_p(dir_path) files.length else - puts Rainbow(" Directory #{dir_path} is already empty").cyan + puts Rainbow(" Directory #{dir_path} is already empty").cyan if verbose FileUtils.rm_rf(dir_path) FileUtils.mkdir_p(dir_path) 0 end end - def create_directory_with_feedback(dir_path) - puts Rainbow(" Directory #{dir_path} does not exist, creating...").cyan + def create_directory_with_feedback(dir_path, verbose: false) + puts Rainbow(" Directory #{dir_path} does not exist, creating...").cyan if verbose FileUtils.mkdir_p(dir_path) 0 end diff --git a/lib/tasks/generate_packs.rake b/lib/tasks/generate_packs.rake index 77d09d8d33..21227453c9 100644 --- a/lib/tasks/generate_packs.rake +++ b/lib/tasks/generate_packs.rake @@ -17,18 +17,24 @@ namespace :react_on_rails do DESC task generate_packs: :environment do - puts Rainbow("๐Ÿš€ Starting React on Rails pack generation...").bold - puts Rainbow("๐Ÿ“ Auto-load bundle: #{ReactOnRails.configuration.auto_load_bundle}").cyan - puts Rainbow("๐Ÿ“‚ Components subdirectory: #{ReactOnRails.configuration.components_subdirectory}").cyan - puts "" + verbose = ENV["REACT_ON_RAILS_VERBOSE"] == "true" + + if verbose + puts Rainbow("๐Ÿš€ Starting React on Rails pack generation...").bold + puts Rainbow("๐Ÿ“ Auto-load bundle: #{ReactOnRails.configuration.auto_load_bundle}").cyan + puts Rainbow("๐Ÿ“‚ Components subdirectory: #{ReactOnRails.configuration.components_subdirectory}").cyan + puts "" + end begin start_time = Time.now ReactOnRails::PacksGenerator.instance.generate_packs_if_stale end_time = Time.now - puts "" - puts Rainbow("โœจ Pack generation completed in #{((end_time - start_time) * 1000).round(1)}ms").green + if verbose + puts "" + puts Rainbow("โœจ Pack generation completed in #{((end_time - start_time) * 1000).round(1)}ms").green + end rescue ReactOnRails::Error => e handle_react_on_rails_error(e) exit 1 diff --git a/sig/react_on_rails/dev/pack_generator.rbs b/sig/react_on_rails/dev/pack_generator.rbs index 76669b35e2..7e5e113949 100644 --- a/sig/react_on_rails/dev/pack_generator.rbs +++ b/sig/react_on_rails/dev/pack_generator.rbs @@ -5,7 +5,7 @@ module ReactOnRails private - def self.run_pack_generation: (?silent: bool) -> bool + def self.run_pack_generation: (?silent: bool, ?verbose: bool) -> bool def self.should_run_directly?: () -> bool def self.rails_available?: () -> bool def self.run_rake_task_directly: (?silent: bool) -> bool @@ -13,7 +13,7 @@ module ReactOnRails def self.prepare_rake_task: () -> untyped def self.capture_output: (bool) { () -> bool } -> bool def self.handle_rake_error: (Exception, bool) -> void - def self.run_via_bundle_exec: (?silent: bool) -> (bool | nil) + def self.run_via_bundle_exec: (?silent: bool, ?verbose: bool) -> (bool | nil) end end end diff --git a/spec/dummy/spec/packs_generator_spec.rb b/spec/dummy/spec/packs_generator_spec.rb index 89ccc7c3a7..7f3177674e 100644 --- a/spec/dummy/spec/packs_generator_spec.rb +++ b/spec/dummy/spec/packs_generator_spec.rb @@ -413,9 +413,12 @@ def self.rsc_support_enabled? it "generate packs if a new component is added" do create_new_component("NewComponent") + # Set verbose mode to see pack generation output + ENV["REACT_ON_RAILS_VERBOSE"] = "true" expect do described_class.instance.generate_packs_if_stale end.to output(GENERATED_PACKS_CONSOLE_OUTPUT_REGEX).to_stdout + ENV.delete("REACT_ON_RAILS_VERBOSE") FileUtils.rm "#{packer_source_path}/components/ComponentWithCommonOnly/ror_components/NewComponent.jsx" end @@ -423,9 +426,12 @@ def self.rsc_support_enabled? FileUtils.rm component_pack create_new_component(component_name) + # Set verbose mode to see pack generation output + ENV["REACT_ON_RAILS_VERBOSE"] = "true" expect do described_class.instance.generate_packs_if_stale end.to output(GENERATED_PACKS_CONSOLE_OUTPUT_REGEX).to_stdout + ENV.delete("REACT_ON_RAILS_VERBOSE") end def create_new_component(name) diff --git a/spec/react_on_rails/dev/pack_generator_spec.rb b/spec/react_on_rails/dev/pack_generator_spec.rb index d4500e5232..f857d90031 100644 --- a/spec/react_on_rails/dev/pack_generator_spec.rb +++ b/spec/react_on_rails/dev/pack_generator_spec.rb @@ -104,6 +104,43 @@ expect(error_output.join("\n")).to match(/Error generating packs: Task failed/) end + it "suggests --verbose flag when pack generation fails in quiet mode" do + allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Task failed")) + + # Mock STDERR.puts to suppress error output + # rubocop:disable Style/GlobalStdStream + allow(STDERR).to receive(:puts) + # rubocop:enable Style/GlobalStdStream + + expect { described_class.generate(verbose: false) } + .to output(/Run with.*--verbose.*flag for detailed output/).to_stdout_from_any_process + .and raise_error(SystemExit) + end + + it "does not suggest --verbose flag when already in verbose mode" do + allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Task failed")) + + # Mock STDERR.puts to suppress error output + # rubocop:disable Style/GlobalStdStream + allow(STDERR).to receive(:puts) + # rubocop:enable Style/GlobalStdStream + + # Capture output to verify --verbose suggestion is not shown + output = StringIO.new + # rubocop:disable RSpec/ExpectOutput + begin + $stdout = output + described_class.generate(verbose: true) + rescue SystemExit + # Expected to exit + ensure + $stdout = STDOUT + end + # rubocop:enable RSpec/ExpectOutput + + expect(output.string).not_to match(/Run with.*--verbose/) + end + it "outputs errors to stderr even in silent mode" do allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Silent mode error")) @@ -140,6 +177,46 @@ expect { described_class.generate(verbose: false) } .not_to output(/This should be suppressed/).to_stdout_from_any_process end + + it "sets REACT_ON_RAILS_VERBOSE environment variable when verbose is true" do + env_value = nil + allow(mock_task).to receive(:invoke) do + env_value = ENV.fetch("REACT_ON_RAILS_VERBOSE", nil) + end + + described_class.generate(verbose: true) + + expect(env_value).to eq("true") + # Ensure cleanup happened + expect(ENV.fetch("REACT_ON_RAILS_VERBOSE", nil)).to be_nil + end + + it "sets REACT_ON_RAILS_VERBOSE environment variable to false when verbose is false" do + env_value = nil + allow(mock_task).to receive(:invoke) do + env_value = ENV.fetch("REACT_ON_RAILS_VERBOSE", nil) + end + + described_class.generate(verbose: false) + + expect(env_value).to eq("false") + # Ensure cleanup happened + expect(ENV.fetch("REACT_ON_RAILS_VERBOSE", nil)).to be_nil + end + + it "cleans up REACT_ON_RAILS_VERBOSE environment variable even when task fails" do + allow(mock_task).to receive(:invoke).and_raise(StandardError.new("Task failed")) + + # Mock STDERR.puts to suppress error output + # rubocop:disable Style/GlobalStdStream + allow(STDERR).to receive(:puts) + # rubocop:enable Style/GlobalStdStream + + expect { described_class.generate(verbose: true) }.to raise_error(SystemExit) + + # Ensure cleanup happened even on failure + expect(ENV.fetch("REACT_ON_RAILS_VERBOSE", nil)).to be_nil + end end context "when not in Bundler context" do @@ -150,19 +227,22 @@ it "runs pack generation successfully in verbose mode using bundle exec" do allow(described_class).to receive(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs") + .with({ "REACT_ON_RAILS_VERBOSE" => "true" }, + "bundle", "exec", "rake", "react_on_rails:generate_packs") .and_return(true) expect { described_class.generate(verbose: true) } .to output(/๐Ÿ“ฆ Generating React on Rails packs.../).to_stdout_from_any_process expect(described_class).to have_received(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs") + .with({ "REACT_ON_RAILS_VERBOSE" => "true" }, + "bundle", "exec", "rake", "react_on_rails:generate_packs") end it "runs pack generation successfully in quiet mode using bundle exec" do allow(described_class).to receive(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs", + .with({ "REACT_ON_RAILS_VERBOSE" => "false" }, + "bundle", "exec", "rake", "react_on_rails:generate_packs", out: File::NULL, err: File::NULL) .and_return(true) @@ -170,13 +250,15 @@ .to output(/๐Ÿ“ฆ Generating packs\.\.\. โœ…/).to_stdout_from_any_process expect(described_class).to have_received(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs", + .with({ "REACT_ON_RAILS_VERBOSE" => "false" }, + "bundle", "exec", "rake", "react_on_rails:generate_packs", out: File::NULL, err: File::NULL) end it "exits with error when pack generation fails" do allow(described_class).to receive(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs", + .with({ "REACT_ON_RAILS_VERBOSE" => "false" }, + "bundle", "exec", "rake", "react_on_rails:generate_packs", out: File::NULL, err: File::NULL) .and_return(false) @@ -196,14 +278,16 @@ it "falls back to bundle exec when Rails is not defined" do allow(described_class).to receive(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs") + .with({ "REACT_ON_RAILS_VERBOSE" => "true" }, + "bundle", "exec", "rake", "react_on_rails:generate_packs") .and_return(true) expect { described_class.generate(verbose: true) } .to output(/๐Ÿ“ฆ Generating React on Rails packs.../).to_stdout_from_any_process expect(described_class).to have_received(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs") + .with({ "REACT_ON_RAILS_VERBOSE" => "true" }, + "bundle", "exec", "rake", "react_on_rails:generate_packs") end end @@ -228,7 +312,8 @@ def self.with_unbundled_env allow(bundler_module).to receive(:with_unbundled_env).and_yield allow(described_class).to receive(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs") + .with({ "REACT_ON_RAILS_VERBOSE" => "true" }, + "bundle", "exec", "rake", "react_on_rails:generate_packs") .and_return(true) described_class.generate(verbose: true) @@ -250,7 +335,8 @@ def self.with_clean_env allow(bundler_module).to receive(:with_clean_env).and_yield allow(described_class).to receive(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs") + .with({ "REACT_ON_RAILS_VERBOSE" => "true" }, + "bundle", "exec", "rake", "react_on_rails:generate_packs") .and_return(true) described_class.generate(verbose: true) @@ -267,28 +353,32 @@ def self.respond_to?(_method, *) stub_const("Bundler", bundler_module) allow(described_class).to receive(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs") + .with({ "REACT_ON_RAILS_VERBOSE" => "true" }, + "bundle", "exec", "rake", "react_on_rails:generate_packs") .and_return(true) expect { described_class.generate(verbose: true) } .to output(/๐Ÿ“ฆ Generating React on Rails packs.../).to_stdout_from_any_process expect(described_class).to have_received(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs") + .with({ "REACT_ON_RAILS_VERBOSE" => "true" }, + "bundle", "exec", "rake", "react_on_rails:generate_packs") end it "executes directly when Bundler is not defined" do hide_const("Bundler") if defined?(Bundler) allow(described_class).to receive(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs") + .with({ "REACT_ON_RAILS_VERBOSE" => "true" }, + "bundle", "exec", "rake", "react_on_rails:generate_packs") .and_return(true) expect { described_class.generate(verbose: true) } .to output(/๐Ÿ“ฆ Generating React on Rails packs.../).to_stdout_from_any_process expect(described_class).to have_received(:system) - .with("bundle", "exec", "rake", "react_on_rails:generate_packs") + .with({ "REACT_ON_RAILS_VERBOSE" => "true" }, + "bundle", "exec", "rake", "react_on_rails:generate_packs") end end end