Skip to content
Merged
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ Changes since the last non-beta release.

[PR 2340](https://github.com/shakacode/react_on_rails/pull/2340) by [justin808](https://github.com/justin808).

#### Fixed

- **Fix generator inheriting BUNDLE_GEMFILE from parent process**: The `react_on_rails:install` generator now wraps bundler commands with `Bundler.with_unbundled_env` to prevent inheriting `BUNDLE_GEMFILE` from the parent process, which caused "injected gems" conflicts when running generators inside a bundled context. [PR 2288](https://github.com/shakacode/react_on_rails/pull/2288) by [ihabadham](https://github.com/ihabadham).

#### Pro

##### Changed
Expand Down
25 changes: 14 additions & 11 deletions react_on_rails/lib/generators/react_on_rails/install_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -218,10 +218,10 @@ def shakapacker_loaded_in_process?(gem_name)
end

def shakapacker_in_lockfile?(gem_name)
gemfile = ENV["BUNDLE_GEMFILE"] || "Gemfile"
lockfile = File.join(File.dirname(gemfile), "Gemfile.lock")

File.file?(lockfile) && File.foreach(lockfile).any? { |l| l.match?(/^\s{4}#{Regexp.escape(gem_name)}\s\(/) }
# Always check the target app's Gemfile.lock, not inherited BUNDLE_GEMFILE
# See: https://github.com/shakacode/react_on_rails/issues/2287
File.file?("Gemfile.lock") &&
File.foreach("Gemfile.lock").any? { |l| l.match?(/^\s{4}#{Regexp.escape(gem_name)}\s\(/) }
end

def shakapacker_in_bundler_specs?(gem_name)
Expand All @@ -232,10 +232,10 @@ def shakapacker_in_bundler_specs?(gem_name)
end

def shakapacker_in_gemfile_text?(gem_name)
gemfile = ENV["BUNDLE_GEMFILE"] || "Gemfile"

File.file?(gemfile) &&
File.foreach(gemfile).any? { |l| l.match?(/^\s*gem\s+['"]#{Regexp.escape(gem_name)}['"]/) }
# Always check the target app's Gemfile, not inherited BUNDLE_GEMFILE
# See: https://github.com/shakacode/react_on_rails/issues/2287
File.file?("Gemfile") &&
File.foreach("Gemfile").any? { |l| l.match?(/^\s*gem\s+['"]#{Regexp.escape(gem_name)}['"]/) }
end

def cli_exists?(command)
Expand Down Expand Up @@ -263,7 +263,9 @@ def ensure_shakapacker_in_gemfile
return if shakapacker_in_gemfile?

puts Rainbow("📝 Adding Shakapacker to Gemfile...").yellow
success = system("bundle add shakapacker --strict")
# Use with_unbundled_env to prevent inheriting BUNDLE_GEMFILE from parent process
# See: https://github.com/shakacode/react_on_rails/issues/2287
success = Bundler.with_unbundled_env { system("bundle add shakapacker --strict") }
return if success

handle_shakapacker_gemfile_error
Expand All @@ -273,15 +275,16 @@ def install_shakapacker
puts Rainbow("⚙️ Installing Shakapacker (required for webpack integration)...").yellow

# First run bundle install to make shakapacker available
# Use with_unbundled_env to prevent inheriting BUNDLE_GEMFILE from parent process
puts Rainbow("📦 Running bundle install...").yellow
bundle_success = system("bundle install")
bundle_success = Bundler.with_unbundled_env { system("bundle install") }
unless bundle_success
handle_shakapacker_install_error
return
end

# Then run the shakapacker installer
success = system("bundle exec rails shakapacker:install")
success = Bundler.with_unbundled_env { system("bundle exec rails shakapacker:install") }
return if success

handle_shakapacker_install_error
Expand Down
8 changes: 6 additions & 2 deletions react_on_rails/rakelib/shakapacker_examples.rake
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength
"echo \"gem 'react_on_rails', path: '#{relative_gem_root}'\" >> #{example_type.gemfile}")
# Shakapacker is automatically included as a dependency via react_on_rails.gemspec (>= 6.0)
bundle_install_in(example_type.dir)
sh_in_dir(example_type.dir, "rake shakapacker:install")
# Use unbundled_sh_in_dir to ensure we're using the generated app's Gemfile
# and gem versions, not the parent workspace's bundle context.
unbundled_sh_in_dir(example_type.dir, "bundle exec rake shakapacker:install")
# Skip validation when running generators on example apps during development.
# The generator validates that certain config options exist in the initializer,
# but during example generation, we're often testing against the current gem
Expand All @@ -97,7 +99,9 @@ namespace :shakapacker_examples do # rubocop:disable Metrics/BlockLength
generator_commands = example_type.generator_shell_commands.map do |cmd|
"REACT_ON_RAILS_SKIP_VALIDATION=true #{cmd}"
end
sh_in_dir(example_type.dir, generator_commands)
# Use unbundled_sh_in_dir to ensure the generator uses the example app's
# gem versions, not the parent workspace's cached bundle context.
unbundled_sh_in_dir(example_type.dir, generator_commands)
# Re-run bundle install since dev_tests generator adds rspec-rails and coveralls to Gemfile
bundle_install_in(example_type.dir)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,4 +496,86 @@
expect(install_generator.send(:missing_node?)).to be true
end
end

# Regression test for https://github.com/shakacode/react_on_rails/issues/2287
# Bundler subprocess commands must run in unbundled environment to prevent
# BUNDLE_GEMFILE inheritance from parent process
describe "bundler environment isolation" do
let(:install_generator) { described_class.new }

it "clears BUNDLE_GEMFILE when running bundle add" do
allow(install_generator).to receive(:shakapacker_in_gemfile?).and_return(false)
allow(install_generator).to receive(:system).with("bundle add shakapacker --strict").and_return(true)

expect(Bundler).to receive(:with_unbundled_env).and_yield

install_generator.send(:ensure_shakapacker_in_gemfile)
end

it "clears BUNDLE_GEMFILE when running bundle install and shakapacker:install" do
# Verify both system calls run inside with_unbundled_env
allow(Bundler).to receive(:with_unbundled_env).and_yield
allow(install_generator).to receive(:system).with("bundle install").and_return(true)
allow(install_generator).to receive(:system).with("bundle exec rails shakapacker:install").and_return(true)

install_generator.send(:install_shakapacker)

expect(install_generator).to have_received(:system).with("bundle install")
expect(install_generator).to have_received(:system).with("bundle exec rails shakapacker:install")
expect(Bundler).to have_received(:with_unbundled_env).at_least(:twice)
end

context "with fake BUNDLE_GEMFILE set" do
around do |example|
original_gemfile = ENV.fetch("BUNDLE_GEMFILE", nil)
example.run
ensure
if original_gemfile
ENV["BUNDLE_GEMFILE"] = original_gemfile
else
ENV.delete("BUNDLE_GEMFILE")
end
end

it "Bundler.with_unbundled_env clears BUNDLE_GEMFILE in block" do
ENV["BUNDLE_GEMFILE"] = "/fake/parent/Gemfile"

bundler_env_in_block = nil
Bundler.with_unbundled_env do
bundler_env_in_block = ENV.fetch("BUNDLE_GEMFILE", nil)
end

expect(bundler_env_in_block).to be_nil
end

it "checks local Gemfile regardless of BUNDLE_GEMFILE env var" do
ENV["BUNDLE_GEMFILE"] = "/some/other/project/Gemfile"

# The method should check "Gemfile" not ENV["BUNDLE_GEMFILE"]
# We verify this by checking it does NOT try to access the env var path
allow(File).to receive(:file?).with("Gemfile").and_return(false)
allow(File).to receive(:file?).with("/some/other/project/Gemfile").and_return(true)

result = install_generator.send(:shakapacker_in_gemfile_text?, "shakapacker")

# If it checked ENV["BUNDLE_GEMFILE"], it would find the file and continue
# Since we return false for "Gemfile", the result should be false
expect(result).to be false
end

it "checks local Gemfile.lock regardless of BUNDLE_GEMFILE env var" do
ENV["BUNDLE_GEMFILE"] = "/some/other/project/Gemfile"

# The method should check "Gemfile.lock" not derived from ENV["BUNDLE_GEMFILE"]
allow(File).to receive(:file?).with("Gemfile.lock").and_return(false)
allow(File).to receive(:file?).with("/some/other/project/Gemfile.lock").and_return(true)

result = install_generator.send(:shakapacker_in_lockfile?, "shakapacker")

# If it derived path from ENV["BUNDLE_GEMFILE"], it would find the file
# Since we return false for "Gemfile.lock", the result should be false
expect(result).to be false
end
end
end
end
Loading