Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 87 additions & 27 deletions lib/demo_scripts/gem_swapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def initialize(gem_paths: {}, github_repos: {}, skip_build: false, watch_mode: f
@skip_build = skip_build
@watch_mode = watch_mode
@spawned_pids = [] # Track PIDs for cleanup on failure
@skipped_gems = [] # Track gems skipped due to missing paths
end

def swap!
Expand All @@ -51,7 +52,7 @@ def swap!

build_local_packages! unless skip_build

puts '✅ Successfully swapped to local gem versions!'
print_swap_summary
print_next_steps
rescue StandardError
# Cleanup spawned watch processes on failure
Expand Down Expand Up @@ -811,19 +812,30 @@ def detect_default_branch(repo)
end

def validate_local_paths!
missing_paths = []

gem_paths.each do |gem_name, path|
next if File.directory?(path)

error_msg = "Local path for #{gem_name} does not exist: #{path}\n\n"
error_msg += "This usually means:\n"
error_msg += " 1. The path in .swap-deps.yml is outdated\n"
error_msg += " 2. You moved or deleted the local repository\n\n"
error_msg += "To fix:\n"
error_msg += " - Update .swap-deps.yml with the correct path\n"
error_msg += ' - Or use --restore to restore original dependencies'
# Skip validation for GitHub-managed repos (they're cloned on demand by clone_github_repos!)
# Note: github_repos take precedence - if a gem is in both gem_paths and github_repos,
# clone_github_repos! will overwrite the gem_paths entry with the cloned cache path
next if github_repos.key?(gem_name)

raise Error, error_msg
missing_paths << { gem_name: gem_name, path: path }
end

return if missing_paths.empty?

# Show warnings for missing paths but don't fail
# This allows graceful handling of deleted Conductor workspaces or stale references
puts "\n⚠️ Warning: Some local paths do not exist:"
missing_paths.each do |missing|
puts " #{missing[:gem_name]}: #{missing[:path]}"
end
puts "\n These paths will be skipped. To fix:"
puts ' - Update .swap-deps.yml with correct paths'
puts " - Or use --restore to restore original dependencies\n\n"
end

def clone_github_repos!
Expand Down Expand Up @@ -889,6 +901,7 @@ def swap_demo(demo_path)
run_npm_install(demo_path) if File.exist?(package_json_path)
end

# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
def swap_gemfile(gemfile_path)
return if gem_paths.empty? && github_repos.empty?

Expand All @@ -901,6 +914,13 @@ def swap_gemfile(gemfile_path)
# Skip if this gem came from GitHub (already in gem_paths via clone_github_repos!)
next if github_repos.key?(gem_name)

# Skip missing local paths (user was already warned by validate_local_paths!)
unless File.directory?(local_path)
puts " ⊘ Skipping #{gem_name} - path does not exist: #{local_path}"
@skipped_gems << gem_name unless @skipped_gems.include?(gem_name)
next
end

content = swap_gem_in_gemfile(content, gem_name, local_path)
end

Expand All @@ -916,33 +936,48 @@ def swap_gemfile(gemfile_path)
puts ' ✓ Updated Gemfile'
end
end
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength

# Strips all source specification parameters (version, path, github, git, branch, tag, ref)
# from a gem line's rest string, leaving only other options (like require: false)
# Returns empty string if no options remain, or the options with leading comma if any remain
def strip_gem_source_params(rest)
options = rest.dup
options = options.sub(/^\s*,\s*(['"])[^'"]*\1/, '') # Remove version if present
options = options.sub(/,\s*path:\s*(['"])[^'"]*\1/, '') # Remove path: if present
options = options.sub(/,\s*github:\s*(['"])[^'"]*\1/, '') # Remove github: if present
options = options.sub(/,\s*git:\s*(['"])[^'"]*\1/, '') # Remove git: if present
options = options.sub(/,\s*branch:\s*(['"])[^'"]*\1/, '') # Remove branch: if present
options = options.sub(/,\s*tag:\s*(['"])[^'"]*\1/, '') # Remove tag: if present
options = options.sub(/,\s*ref:\s*(['"])[^'"]*\1/, '') # Remove ref: if present

# Return empty string if only whitespace/commas remain, otherwise return with leading comma
options.strip.empty? ? '' : options
end

def swap_gem_in_gemfile(content, gem_name, local_path)
# Match variations:
# gem 'name', '~> 1.0'
# gem "name", "~> 1.0", require: false
# gem 'name' (no version)
# gem 'name', require: false (no version, with options)
# BUT NOT: gem 'name', path: '...' (already swapped - skip these)
# gem 'name', path: '...' (existing path - will be replaced)
# gem 'name', github: '...' (existing github - will be replaced)

# Simple pattern: match gem lines for this gem name
pattern = /^(\s*)gem\s+(['"])#{Regexp.escape(gem_name)}\2(.*)$/

content.gsub(pattern) do |match|
# Skip if line already contains 'path:' or 'github:' - already swapped
next match if match.include?('path:') || match.include?('github:')

content.gsub(pattern) do |_match|
indent = Regexp.last_match(1)
quote = Regexp.last_match(2)
rest = Regexp.last_match(3)

# Extract options after version (if any)
# Match: , 'version', options OR , options OR nothing
options = rest.sub(/^\s*,\s*(['"])[^'"]*\1/, '') # Remove version if present
# Extract options after stripping all source specification parameters
options = strip_gem_source_params(rest)

# Build replacement: gem 'name', path: 'local_path' [, options...]
replacement = "#{indent}gem #{quote}#{gem_name}#{quote}, path: #{quote}#{local_path}#{quote}"
replacement += options unless options.strip.empty?
replacement += options unless options.empty?
replacement
end
end
Expand All @@ -951,16 +986,13 @@ def swap_gem_to_github(content, gem_name, info)
# Match gem lines for this gem name
pattern = /^(\s*)gem\s+(['"])#{Regexp.escape(gem_name)}\2(.*)$/

content.gsub(pattern) do |match|
# Skip if line already contains 'path:' or 'github:' - already swapped
next match if match.include?('path:') || match.include?('github:')

content.gsub(pattern) do |_match|
indent = Regexp.last_match(1)
quote = Regexp.last_match(2)
rest = Regexp.last_match(3)

# Extract options after version (if any)
options = rest.sub(/^\s*,\s*(['"])[^'"]*\1/, '') # Remove version if present
# Extract options after stripping all source specification parameters
options = strip_gem_source_params(rest)

# Use tag: for tags, branch: for branches (default to :branch if not specified)
ref_type = info[:ref_type] || :branch
Expand All @@ -973,12 +1005,12 @@ def swap_gem_to_github(content, gem_name, info)
# Build replacement: gem 'name', github: 'user/repo', branch/tag: 'ref-name' [, options...]
replacement = "#{indent}gem #{quote}#{gem_name}#{quote}, github: #{quote}#{info[:repo]}#{quote}"
replacement += ", #{param_name}: #{quote}#{info[:branch]}#{quote}" unless should_omit_ref
replacement += options unless options.strip.empty?
replacement += options unless options.empty?
replacement
end
end

# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
def swap_package_json(package_json_path)
npm_gems = gem_paths.select { |gem_name, _| NPM_PACKAGE_PATHS[gem_name] }
return if npm_gems.empty?
Expand All @@ -992,6 +1024,13 @@ def swap_package_json(package_json_path)
npm_package_path = NPM_PACKAGE_PATHS[gem_name]
next if npm_package_path.nil?

# Skip missing local paths (user was already warned by validate_local_paths!)
unless File.directory?(local_path)
puts " ⊘ Skipping #{gem_name} npm package - path does not exist: #{local_path}"
@skipped_gems << gem_name unless @skipped_gems.include?(gem_name)
next
end

full_npm_path = File.join(local_path, npm_package_path)
npm_name = gem_name.tr('_', '-') # Convert snake_case to kebab-case

Expand All @@ -1006,7 +1045,7 @@ def swap_package_json(package_json_path)

write_file(package_json_path, "#{JSON.pretty_generate(data)}\n") if modified
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength

# rubocop:disable Metrics/MethodLength
def restore_demo(demo_path)
Expand Down Expand Up @@ -1282,6 +1321,13 @@ def build_local_packages!
npm_package_path = NPM_PACKAGE_PATHS[gem_name]
next if npm_package_path.nil?

# Skip missing local paths (user was already warned by validate_local_paths!)
unless File.directory?(local_path)
puts " ⊘ Skipping #{gem_name} build - path does not exist: #{local_path}"
@skipped_gems << gem_name unless @skipped_gems.include?(gem_name)
next
end

build_npm_package(gem_name, local_path, npm_package_path)
end
end
Expand Down Expand Up @@ -1326,6 +1372,20 @@ def build_npm_package(gem_name, gem_root, npm_subdir)
end
# rubocop:enable Metrics/MethodLength

def print_swap_summary
swapped_gems = gem_paths.keys - @skipped_gems

puts "\n✅ Successfully swapped to local gem versions!"
puts "\n📊 Summary:"
puts " ✓ Swapped: #{swapped_gems.size} gem(s)"
swapped_gems.each { |gem| puts " - #{gem}" } if swapped_gems.any?

return unless @skipped_gems.any?

puts " ⊘ Skipped: #{@skipped_gems.size} gem(s) (missing paths)"
@skipped_gems.each { |gem| puts " - #{gem}" }
end

def print_next_steps
puts "\n📝 Next steps:"
puts ' 1. Local packages are now linked via file: protocol'
Expand Down
Loading