diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dd0b13a14..800f7cddf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,10 @@ Changes since the last non-beta release. - **CSP Nonce Support for Console Replay**: Added Content Security Policy (CSP) nonce support for the `consoleReplay` script generated during server-side rendering. When Rails CSP is configured, the console replay script will automatically include the nonce attribute, allowing it to execute under restrictive CSP policies like `script-src: 'self'`. The implementation includes cross-version Rails compatibility (5.2-7.x) and defense-in-depth nonce sanitization to prevent attribute injection attacks. [PR 2059](https://github.com/shakacode/react_on_rails/pull/2059) by [justin808](https://github.com/justin808). +#### Bug Fixes + +- [PR 2085](https://github.com/shakacode/react_on_rails/pull/2085) by [justin808](https://github.com/justin808): Fix pack generation in bin/dev when running from Bundler context. Pack generation was failing with "Could not find command 'react_on_rails:generate_packs'" because Bundler was intercepting the subprocess call. The fix wraps the bundle exec call with `Bundler.with_unbundled_env` to prevent interception. + ### [v16.2.0.beta.11] - 2025-11-19 #### Added diff --git a/lib/react_on_rails/dev/pack_generator.rb b/lib/react_on_rails/dev/pack_generator.rb index 48e1f4eb4a..5044984050 100644 --- a/lib/react_on_rails/dev/pack_generator.rb +++ b/lib/react_on_rails/dev/pack_generator.rb @@ -141,13 +141,34 @@ def handle_rake_error(error, _silent) end def run_via_bundle_exec(silent: false) - if silent - system( - "bundle", "exec", "rake", "react_on_rails:generate_packs", - out: File::NULL, err: File::NULL - ) + # 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( + "bundle", "exec", "rake", "react_on_rails:generate_packs", + out: File::NULL, err: File::NULL + ) + else + system("bundle", "exec", "rake", "react_on_rails:generate_packs") + end + end + end + + # DRY helper method for Bundler context switching with API compatibility + # Supports both new (with_unbundled_env) and legacy (with_clean_env) Bundler APIs + def with_unbundled_context(&block) + if defined?(Bundler) + if Bundler.respond_to?(:with_unbundled_env) + Bundler.with_unbundled_env(&block) + elsif Bundler.respond_to?(:with_clean_env) + Bundler.with_clean_env(&block) + else + # Fallback if neither method is available (very old Bundler versions) + yield + end else - system("bundle", "exec", "rake", "react_on_rails:generate_packs") + yield end end end diff --git a/spec/react_on_rails/dev/pack_generator_spec.rb b/spec/react_on_rails/dev/pack_generator_spec.rb index 751725eccb..d4500e5232 100644 --- a/spec/react_on_rails/dev/pack_generator_spec.rb +++ b/spec/react_on_rails/dev/pack_generator_spec.rb @@ -206,5 +206,90 @@ .with("bundle", "exec", "rake", "react_on_rails:generate_packs") end end + + context "when calling bundle exec from within Bundler context" do + before do + # Ensure we're not in Rails context to trigger bundle exec path + hide_const("Rails") if defined?(Rails) + allow(ReactOnRails::PackerUtils).to receive(:shakapacker_precompile_hook_configured?).and_return(false) + end + + it "unwraps the Bundler context before executing with with_unbundled_env" do + bundler_module = Module.new do + def self.respond_to?(method, *) + method == :with_unbundled_env + end + + def self.with_unbundled_env + yield + end + end + stub_const("Bundler", bundler_module) + + 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") + .and_return(true) + + described_class.generate(verbose: true) + + expect(bundler_module).to have_received(:with_unbundled_env) + end + + it "falls back to with_clean_env when with_unbundled_env is not available" do + bundler_module = Module.new do + def self.respond_to?(method, *) + method == :with_clean_env + end + + def self.with_clean_env + yield + end + end + stub_const("Bundler", bundler_module) + + 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") + .and_return(true) + + described_class.generate(verbose: true) + + expect(bundler_module).to have_received(:with_clean_env) + end + + it "executes directly when neither with_unbundled_env nor with_clean_env are available" do + bundler_module = Module.new do + def self.respond_to?(_method, *) + false + end + end + stub_const("Bundler", bundler_module) + + allow(described_class).to receive(:system) + .with("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") + 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") + .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") + end + end end end