Skip to content

Commit 135ae13

Browse files
authored
Fix lock file updates when restoring dependencies (#49)
* Fix lock file updates when restoring dependencies When swap-deps restores original dependencies, the lock files (Gemfile.lock and package-lock.json) were not properly updated to reflect the restored versions. This caused them to retain references to local paths even after restore. Changes: - For Bundler: Use `bundle update` for specific gems during restore to force re-resolution from rubygems - For npm: Remove package-lock.json before install to force re-resolution from npm registry This ensures lock files are properly updated when restoring from local/path dependencies back to registry versions.
1 parent 8deb1a2 commit 135ae13

File tree

2 files changed

+207
-12
lines changed

2 files changed

+207
-12
lines changed

lib/demo_scripts/gem_swapper.rb

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,7 @@ def swap_package_json(package_json_path)
713713
end
714714
# rubocop:enable Metrics/AbcSize
715715

716+
# rubocop:disable Metrics/MethodLength
716717
def restore_demo(demo_path)
717718
restored = 0
718719
gemfile_path = File.join(demo_path, 'Gemfile')
@@ -733,12 +734,17 @@ def restore_demo(demo_path)
733734
end
734735

735736
if restored.positive?
736-
run_bundle_install(demo_path) if File.exist?(gemfile_path)
737-
run_npm_install(demo_path) if File.exist?(package_json_path)
737+
bundle_success = File.exist?(gemfile_path) ? run_bundle_install(demo_path, for_restore: true) : true
738+
npm_success = File.exist?(package_json_path) ? run_npm_install(demo_path, for_restore: true) : true
739+
740+
unless bundle_success && npm_success
741+
warn ' ⚠️ Warning: Some dependency installations failed. Check the errors above.'
742+
end
738743
end
739744

740745
restored
741746
end
747+
# rubocop:enable Metrics/MethodLength
742748

743749
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
744750
def backup_file(file_path)
@@ -808,27 +814,100 @@ def write_file(file_path, content)
808814
end
809815
end
810816

811-
def run_bundle_install(demo_path)
817+
def find_supported_gems_in_gemfile(demo_path)
818+
gemfile_content = File.read(File.join(demo_path, 'Gemfile'))
819+
SUPPORTED_GEMS.select do |gem_name|
820+
# Match: gem 'name' or gem "name" at line start (avoiding false matches in comments)
821+
# Note: Regex compilation per gem is negligible for our 3 supported gems
822+
gemfile_content.match?(/^\s*gem\s+["']#{Regexp.escape(gem_name)}["']/)
823+
end
824+
end
825+
826+
# rubocop:disable Metrics/MethodLength
827+
def run_bundle_install(demo_path, for_restore: false)
812828
return if dry_run
813829

814-
puts ' Running bundle install...'
815-
success = Dir.chdir(demo_path) do
816-
system('bundle', 'install', '--quiet')
830+
if for_restore
831+
# For restore, we need to update the gems to fetch from rubygems
832+
# This ensures Gemfile.lock is properly updated
833+
puts ' Running bundle update (to restore gem sources)...'
834+
835+
gems_to_update = find_supported_gems_in_gemfile(demo_path)
836+
837+
if gems_to_update.empty?
838+
# No supported gems found in Gemfile - this might indicate they were never swapped
839+
# or the Gemfile structure is unexpected
840+
puts ' ⚠️ No swapped gems detected in Gemfile. Running standard bundle install...'
841+
success = Dir.chdir(demo_path) do
842+
system('bundle', 'install', '--quiet')
843+
end
844+
else
845+
puts " Updating gems: #{gems_to_update.join(', ')}" if verbose
846+
success = Dir.chdir(demo_path) do
847+
# Update specific gems to pull from rubygems
848+
result = system('bundle', 'update', *gems_to_update, '--quiet')
849+
warn ' ⚠️ ERROR: Failed to update gems. Lock file may be inconsistent.' unless result
850+
result
851+
end
852+
end
853+
else
854+
puts ' Running bundle install...'
855+
success = Dir.chdir(demo_path) do
856+
system('bundle', 'install', '--quiet')
857+
end
817858
end
818859

819-
warn ' ⚠️ Warning: bundle install failed' unless success
860+
warn ' ⚠️ ERROR: bundle command failed' unless success
861+
success
820862
end
863+
# rubocop:enable Metrics/MethodLength
821864

822-
def run_npm_install(demo_path)
865+
# rubocop:disable Metrics/MethodLength
866+
def run_npm_install(demo_path, for_restore: false)
823867
return if dry_run
824868

825-
puts ' Running npm install...'
826-
success = Dir.chdir(demo_path) do
827-
system('npm', 'install', '--silent', out: '/dev/null', err: '/dev/null')
869+
if for_restore
870+
# For restore, we need to regenerate package-lock.json from package.json
871+
# to fetch from npm registry instead of local file: paths
872+
puts ' Running npm install (regenerating lock file)...'
873+
874+
package_lock_path = File.join(demo_path, 'package-lock.json')
875+
package_lock_backup = "#{package_lock_path}.backup"
876+
877+
# Atomically move package-lock.json to backup to avoid race conditions
878+
begin
879+
File.rename(package_lock_path, package_lock_backup)
880+
puts ' Moved package-lock.json to backup for regeneration' if verbose
881+
rescue Errno::ENOENT
882+
# File doesn't exist, which is fine - nothing to backup
883+
puts ' No package-lock.json found to backup' if verbose
884+
end
885+
886+
success = Dir.chdir(demo_path) do
887+
# Use npm install to regenerate package-lock.json from package.json
888+
# Don't use npm ci since we just deleted package-lock.json
889+
system('npm', 'install', '--silent', out: '/dev/null', err: '/dev/null')
890+
end
891+
892+
if success
893+
# Remove backup on success
894+
FileUtils.rm_f(package_lock_backup)
895+
elsif File.exist?(package_lock_backup)
896+
# Restore backup on failure
897+
FileUtils.mv(package_lock_backup, package_lock_path)
898+
warn ' ⚠️ ERROR: npm install failed. Restored original package-lock.json'
899+
end
900+
else
901+
puts ' Running npm install...'
902+
success = Dir.chdir(demo_path) do
903+
system('npm', 'install', '--silent', out: '/dev/null', err: '/dev/null')
904+
end
828905
end
829906

830-
warn ' ⚠️ Warning: npm install failed' unless success
907+
warn ' ⚠️ ERROR: npm install failed' unless success
908+
success
831909
end
910+
# rubocop:enable Metrics/MethodLength
832911

833912
def build_local_packages!
834913
return if dry_run

spec/demo_scripts/gem_swapper_spec.rb

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,4 +721,120 @@
721721
end.to output(//).to_stdout
722722
end
723723
end
724+
725+
describe '#run_bundle_install with restore' do
726+
let(:demo_path) { '/path/to/demo' }
727+
let(:gemfile_path) { File.join(demo_path, 'Gemfile') }
728+
729+
before do
730+
allow(swapper).to receive(:dry_run).and_return(false)
731+
end
732+
733+
context 'when for_restore is true' do
734+
it 'runs bundle update for supported gems' do
735+
gemfile_content = <<~GEMFILE
736+
gem 'rails'
737+
gem 'shakapacker', '~> 9.0'
738+
gem 'react_on_rails'
739+
GEMFILE
740+
741+
allow(File).to receive(:read).with(gemfile_path).and_return(gemfile_content)
742+
expect(Dir).to receive(:chdir).with(demo_path).and_yield
743+
expect(swapper).to receive(:system)
744+
.with('bundle', 'update', 'shakapacker', 'react_on_rails', '--quiet').and_return(true)
745+
746+
result = swapper.send(:run_bundle_install, demo_path, for_restore: true)
747+
expect(result).to be true
748+
end
749+
750+
it 'falls back to bundle install when no supported gems found' do
751+
gemfile_content = "gem 'rails'\ngem 'pg'"
752+
753+
allow(File).to receive(:read).with(gemfile_path).and_return(gemfile_content)
754+
expect(Dir).to receive(:chdir).with(demo_path).and_yield
755+
expect(swapper).to receive(:system).with('bundle', 'install', '--quiet').and_return(true)
756+
757+
swapper.send(:run_bundle_install, demo_path, for_restore: true)
758+
end
759+
760+
it 'returns false and warns on failure' do
761+
gemfile_content = "gem 'shakapacker'"
762+
763+
allow(File).to receive(:read).with(gemfile_path).and_return(gemfile_content)
764+
expect(Dir).to receive(:chdir).with(demo_path).and_yield
765+
expect(swapper).to receive(:system).with('bundle', 'update', 'shakapacker', '--quiet').and_return(false)
766+
expect(swapper).to receive(:warn).with(/ERROR: Failed to update gems/)
767+
expect(swapper).to receive(:warn).with(/ERROR: bundle command failed/)
768+
769+
result = swapper.send(:run_bundle_install, demo_path, for_restore: true)
770+
expect(result).to be false
771+
end
772+
end
773+
774+
context 'when for_restore is false' do
775+
it 'runs regular bundle install' do
776+
expect(Dir).to receive(:chdir).with(demo_path).and_yield
777+
expect(swapper).to receive(:system).with('bundle', 'install', '--quiet').and_return(true)
778+
779+
swapper.send(:run_bundle_install, demo_path, for_restore: false)
780+
end
781+
end
782+
end
783+
784+
describe '#run_npm_install with restore' do
785+
let(:demo_path) { '/path/to/demo' }
786+
let(:package_lock_path) { File.join(demo_path, 'package-lock.json') }
787+
let(:package_lock_backup) { "#{package_lock_path}.backup" }
788+
789+
before do
790+
allow(swapper).to receive(:dry_run).and_return(false)
791+
end
792+
793+
context 'when for_restore is true' do
794+
it 'backs up and removes package-lock.json before install' do
795+
expect(File).to receive(:rename).with(package_lock_path, package_lock_backup)
796+
expect(Dir).to receive(:chdir).with(demo_path).and_yield
797+
expect(swapper).to receive(:system)
798+
.with('npm', 'install', '--silent', out: '/dev/null', err: '/dev/null').and_return(true)
799+
expect(FileUtils).to receive(:rm_f).with(package_lock_backup)
800+
801+
swapper.send(:run_npm_install, demo_path, for_restore: true)
802+
end
803+
804+
it 'restores backup on failure' do
805+
allow(File).to receive(:exist?).with(package_lock_backup).and_return(true)
806+
expect(File).to receive(:rename).with(package_lock_path, package_lock_backup)
807+
expect(Dir).to receive(:chdir).with(demo_path).and_yield
808+
expect(swapper).to receive(:system)
809+
.with('npm', 'install', '--silent', out: '/dev/null', err: '/dev/null').and_return(false)
810+
expect(FileUtils).to receive(:mv).with(package_lock_backup, package_lock_path)
811+
expect(swapper).to receive(:warn).with(/ERROR: npm install failed/)
812+
expect(swapper).to receive(:warn).with(/ERROR: npm install failed/)
813+
814+
swapper.send(:run_npm_install, demo_path, for_restore: true)
815+
end
816+
817+
it 'handles missing package-lock.json gracefully' do
818+
expect(File).to receive(:rename)
819+
.with(package_lock_path, package_lock_backup)
820+
.and_raise(Errno::ENOENT)
821+
expect(FileUtils).to receive(:rm_f).with(package_lock_backup)
822+
expect(Dir).to receive(:chdir).with(demo_path).and_yield
823+
expect(swapper).to receive(:system)
824+
.with('npm', 'install', '--silent', out: '/dev/null', err: '/dev/null').and_return(true)
825+
826+
swapper.send(:run_npm_install, demo_path, for_restore: true)
827+
end
828+
end
829+
830+
context 'when for_restore is false' do
831+
it 'runs regular npm install' do
832+
expect(Dir).to receive(:chdir).with(demo_path).and_yield
833+
expect(swapper).to receive(:system)
834+
.with('npm', 'install', '--silent', out: '/dev/null', err: '/dev/null').and_return(true)
835+
836+
swapper.send(:run_npm_install, demo_path, for_restore: false)
837+
end
838+
end
839+
end
724840
end

0 commit comments

Comments
 (0)