Skip to content

Commit c7af518

Browse files
justin808claude
andauthored
Add assets_bundler to config if missing during bundler switch (#833)
## Summary - Fixes an issue where `rake shakapacker:switch_bundler rspack --install-deps` silently fails when `assets_bundler` is missing from `shakapacker.yml` - Automatically adds the `assets_bundler` key with appropriate comments when missing - Prevents early exit when the key is missing but the default value matches the target bundler ## Key Changes 1. **Detection**: Check if `assets_bundler` key exists before early exit logic 2. **Addition**: Add the key intelligently based on config structure: - After `javascript_transpiler` if present - After `source_path` if `javascript_transpiler` missing - After `default: &default` as last resort 3. **Preservation**: Maintain existing behavior for configs that already have the key ## Test Plan - Added test for adding `assets_bundler` after `javascript_transpiler` - Added test for adding `assets_bundler` when `javascript_transpiler` is missing - All existing tests pass - RuboCop passes ## Impact This fix is particularly helpful for users migrating from older Shakapacker versions where `assets_bundler` wasn't part of the default config. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Enhanced bundler switching with configuration validation to ensure correct assets_bundler setup when switching between rspack and webpack. * Improved configuration file handling with better placement logic and comment preservation. * **Bug Fixes** * Fixed handling of commented-out configuration entries during bundler switches. * Enhanced edge-case support for non-standard configuration structures. * **Tests** * Added comprehensive test coverage for bundler switching scenarios and configuration updates. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b737dbb commit c7af518

2 files changed

Lines changed: 305 additions & 10 deletions

File tree

lib/shakapacker/bundler_switcher.rb

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ class BundlerSwitcher
99
SHAKAPACKER_CONFIG = "config/shakapacker.yml"
1010
CUSTOM_DEPS_CONFIG = ".shakapacker-switch-bundler-dependencies.yml"
1111

12+
# Regex pattern to detect assets_bundler key in config (only matches uncommented lines)
13+
ASSETS_BUNDLER_PATTERN = /^[ \t]*assets_bundler:/
14+
1215
# Default dependencies for each bundler (package names only, no versions)
1316
DEFAULT_RSPACK_DEPS = {
1417
dev: %w[@rspack/cli @rspack/plugin-react-refresh],
@@ -37,18 +40,33 @@ def switch_to(bundler, install_deps: false, no_uninstall: false)
3740
end
3841

3942
current = current_bundler
40-
if current == bundler && !install_deps
43+
config_content = File.read(config_path)
44+
has_assets_bundler = config_content =~ ASSETS_BUNDLER_PATTERN
45+
46+
# Early exit if already using the target bundler
47+
# For webpack: if current is webpack, we're done (key optional due to default)
48+
# For rspack: requires explicit key to be present
49+
already_configured = if bundler == "webpack"
50+
current == bundler
51+
else
52+
current == bundler && has_assets_bundler
53+
end
54+
55+
if already_configured && !install_deps
4156
puts "✅ Already using #{bundler}"
4257
return
4358
end
4459

45-
if current == bundler && install_deps
60+
if already_configured && install_deps
4661
puts "✅ Already using #{bundler} - reinstalling dependencies as requested"
4762
manage_dependencies(bundler, install_deps, switching: false, no_uninstall: no_uninstall)
4863
return
4964
end
5065

51-
update_config(bundler)
66+
successfully_updated = update_config(bundler, config_content, has_assets_bundler)
67+
68+
# Verify the update was successful (only if update reported success)
69+
verify_config_update(bundler) if successfully_updated
5270

5371
puts "✅ Switched from #{current} to #{bundler}"
5472
puts ""
@@ -143,20 +161,74 @@ def load_dependencies
143161
end
144162
end
145163

146-
def update_config(bundler)
147-
content = File.read(config_path)
164+
def update_config(bundler, content, has_assets_bundler)
165+
# Check if assets_bundler key exists (only uncommented lines)
166+
unless has_assets_bundler
167+
# Track whether we successfully added the key
168+
added = false
169+
170+
# Add assets_bundler after javascript_transpiler if it exists (excluding commented lines)
171+
if (match = content.match(/^[ \t]*(?![ \t]*#)javascript_transpiler:.*$/))
172+
indent = match[0][/^[ \t]*/]
173+
content.sub!(/^([ \t]*(?![ \t]*#)javascript_transpiler:.*$)/, "\\1\n#{assets_bundler_entry(bundler, indent)}")
174+
added = true
175+
# Otherwise, add it after source_path if it exists (excluding commented lines)
176+
elsif (match = content.match(/^[ \t]*(?![ \t]*#)source_path:.*$/))
177+
indent = match[0][/^[ \t]*/]
178+
content.sub!(/^([ \t]*(?![ \t]*#)source_path:.*$)/, "\\1\n#{assets_bundler_entry(bundler, indent)}")
179+
added = true
180+
# Add it after default: &default if it exists
181+
elsif content.match?(/^default:[ \t]*&default[ \t]*$/)
182+
# Use default 2-space indentation for this case
183+
content.sub!(/^(default:[ \t]*&default[ \t]*)$/, "\\1\n#{assets_bundler_entry(bundler, ' ')}")
184+
added = true
185+
# Fallback: add after "default:" with proper indentation detection (handles blank lines)
186+
elsif (match = content.match(/^default:\s*\n\s*([ \t]+)/m))
187+
# Extract indentation from first indented line after "default:"
188+
indent = match[1]
189+
content.sub!(/^(default:\s*)$/, "\\1\n#{assets_bundler_entry(bundler, indent)}")
190+
added = true
191+
end
148192

149-
# Replace assets_bundler value (handles spaces, tabs, and various quote styles)
150-
# Only matches uncommented lines
151-
content.gsub!(/^([ \t]*assets_bundler:[ \t]*['"]?)(webpack|rspack)(['"]?)/, "\\1#{bundler}\\3")
193+
unless added
194+
puts "⚠️ Warning: Could not find appropriate location for assets_bundler in config"
195+
puts " Please add 'assets_bundler: #{bundler}' to the default section manually"
196+
end
197+
else
198+
# Replace existing assets_bundler value (handles spaces, tabs, and various quote styles)
199+
# Only matches uncommented lines
200+
content.gsub!(/^([ \t]*)(?![ \t]*#)(assets_bundler:[ \t]*['"]?)(webpack|rspack)(['"]?)/, "\\1\\2#{bundler}\\4")
201+
added = true
202+
end
152203

153204
# Update javascript_transpiler recommendation for rspack
154205
# Only update if not already set to swc and only on uncommented lines
155-
if bundler == "rspack" && content !~ /^[ \t]*javascript_transpiler:[ \t]*['"]?swc['"]?/
156-
content.gsub!(/^([ \t]*javascript_transpiler:[ \t]*['"]?)\w+(['"]?)/, "\\1swc\\2")
206+
if bundler == "rspack" && content !~ /^[ \t]*(?![ \t]*#)javascript_transpiler:[ \t]*['"]?swc['"]?/
207+
content.gsub!(/^([ \t]*(?![ \t]*#)javascript_transpiler:[ \t]*['"]?)(\w+)(['"]?)/, '\1swc\3')
157208
end
158209

159210
File.write(config_path, content)
211+
added
212+
end
213+
214+
# Verify that the config was updated successfully
215+
def verify_config_update(bundler)
216+
config = load_yaml_config(config_path)
217+
actual_bundler = config.dig("default", "assets_bundler")
218+
219+
if actual_bundler != bundler
220+
raise "Config update verification failed: expected assets_bundler to be '#{bundler}', but got '#{actual_bundler}'"
221+
end
222+
rescue Psych::SyntaxError => e
223+
raise "Config update generated invalid YAML: #{e.message}"
224+
end
225+
226+
# Generate the assets_bundler YAML entry with proper indentation
227+
# @param bundler [String] The bundler name ('webpack' or 'rspack')
228+
# @param indent [String] The indentation string to use (e.g., ' ' or '\t')
229+
# @return [String] The formatted YAML entry
230+
def assets_bundler_entry(bundler, indent)
231+
"\n#{indent}# Select assets bundler to use\n#{indent}# Available options: 'webpack' (default) or 'rspack'\n#{indent}assets_bundler: \"#{bundler}\""
160232
end
161233

162234
def manage_dependencies(bundler, install_deps, switching: true, no_uninstall: false)

spec/shakapacker/bundler_switcher_spec.rb

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,120 @@ def load_yaml_for_test(path)
137137
expect(config["default"]["javascript_transpiler"]).to eq("swc")
138138
end
139139

140+
it "adds assets_bundler key when missing from config" do
141+
config_without_assets_bundler = <<~YAML
142+
default: &default
143+
source_path: app/javascript
144+
javascript_transpiler: babel
145+
146+
development:
147+
<<: *default
148+
149+
production:
150+
<<: *default
151+
YAML
152+
File.write(config_path, config_without_assets_bundler)
153+
154+
switcher.switch_to("rspack")
155+
config = load_yaml_for_test(config_path)
156+
expect(config["default"]["assets_bundler"]).to eq("rspack")
157+
end
158+
159+
it "does not add assets_bundler key when switching to webpack without the key (using default)" do
160+
config_minimal = <<~YAML
161+
default: &default
162+
source_path: app/javascript
163+
164+
development:
165+
<<: *default
166+
YAML
167+
File.write(config_path, config_minimal)
168+
169+
# When switching to webpack without the key, it should recognize we're already using webpack
170+
# via the default and not add an unnecessary key
171+
expect do
172+
switcher.switch_to("webpack")
173+
end.to output(/Already using webpack/).to_stdout
174+
175+
config = load_yaml_for_test(config_path)
176+
# Should NOT have added the key since we're already on webpack via default
177+
expect(config.dig("default", "assets_bundler")).to be_nil
178+
end
179+
180+
it "adds assets_bundler key when switching to rspack even without javascript_transpiler" do
181+
config_minimal = <<~YAML
182+
default: &default
183+
source_path: app/javascript
184+
185+
development:
186+
<<: *default
187+
YAML
188+
File.write(config_path, config_minimal)
189+
190+
# When switching to rspack, the key should be added even without javascript_transpiler
191+
switcher.switch_to("rspack")
192+
config = load_yaml_for_test(config_path)
193+
expect(config["default"]["assets_bundler"]).to eq("rspack")
194+
end
195+
196+
it "adds assets_bundler key to real-world config structure" do
197+
# Based on https://github.com/swrobel/meta-surf-forecast/blob/8f54a2ca0a798b8724430e137d56dfcc5fcbe775/config/shakapacker.yml
198+
config_real_world = <<~YAML
199+
# Note: You must restart bin/webpack-dev-server for changes to take effect
200+
201+
default: &default
202+
ensure_consistent_versioning: true
203+
source_path: app/javascript
204+
source_entry_path: /
205+
public_root_path: public
206+
public_output_path: packs
207+
cache_path: tmp/cache/webpacker
208+
check_yarn_integrity: false
209+
webpack_compile_output: false
210+
javascript_transpiler: 'swc'
211+
212+
# If nested_entries is true, then we'll pick up subdirectories within the source_entry_path.
213+
nested_entries: false
214+
215+
development:
216+
<<: *default
217+
compile: true
218+
219+
production:
220+
<<: *default
221+
compile: false
222+
YAML
223+
File.write(config_path, config_real_world)
224+
225+
switcher.switch_to("rspack")
226+
config = load_yaml_for_test(config_path)
227+
expect(config["default"]["assets_bundler"]).to eq("rspack")
228+
# Verify it was added after javascript_transpiler with proper indentation
229+
content = File.read(config_path)
230+
expect(content).to match(/javascript_transpiler:.*\n\n.*assets_bundler:/m)
231+
end
232+
233+
it "treats commented-out assets_bundler as missing" do
234+
config_with_commented_key = <<~YAML
235+
default: &default
236+
source_path: app/javascript
237+
javascript_transpiler: babel
238+
# assets_bundler: webpack
239+
240+
development:
241+
<<: *default
242+
YAML
243+
File.write(config_path, config_with_commented_key)
244+
245+
switcher.switch_to("rspack")
246+
config = load_yaml_for_test(config_path)
247+
# Should add a new uncommented key
248+
expect(config["default"]["assets_bundler"]).to eq("rspack")
249+
# Should preserve the commented line
250+
content = File.read(config_path)
251+
expect(content).to include("# assets_bundler: webpack")
252+
end
253+
140254
it "preserves config file structure and comments" do
141255
config_with_comments = <<~YAML
142256
# This is a comment
@@ -156,6 +270,115 @@ def load_yaml_for_test(path)
156270
expect(updated_content).to include("# inline comment")
157271
end
158272

273+
it "handles non-standard config with fallback pattern" do
274+
# Config with no javascript_transpiler, source_path, or standard anchor
275+
config_non_standard = <<~YAML
276+
default: &default
277+
public_root_path: public
278+
public_output_path: packs
279+
cache_path: tmp/cache/webpacker
280+
281+
development:
282+
<<: *default
283+
YAML
284+
File.write(config_path, config_non_standard)
285+
286+
# Should use the fallback pattern to add after "default:" with detected indentation
287+
switcher.switch_to("rspack")
288+
config = load_yaml_for_test(config_path)
289+
expect(config["default"]["assets_bundler"]).to eq("rspack")
290+
end
291+
292+
it "warns when config structure is completely incompatible" do
293+
# Completely unusual structure with no recognizable patterns
294+
config_incompatible = <<~YAML
295+
production_only: &production
296+
cache_path: tmp/cache
297+
YAML
298+
File.write(config_path, config_incompatible)
299+
300+
expect do
301+
switcher.switch_to("rspack")
302+
end.to output(/Warning: Could not find appropriate location/).to_stdout
303+
304+
# Should not have added the key since structure was incompatible
305+
config = load_yaml_for_test(config_path)
306+
expect(config.dig("default", "assets_bundler")).to be_nil
307+
end
308+
309+
it "does not match commented javascript_transpiler lines" do
310+
config_with_commented = <<~YAML
311+
default: &default
312+
source_path: app/javascript
313+
# javascript_transpiler: babel
314+
other_key: value
315+
316+
development:
317+
<<: *default
318+
YAML
319+
File.write(config_path, config_with_commented)
320+
321+
switcher.switch_to("rspack")
322+
content = File.read(config_path)
323+
# Should not add after the commented line
324+
expect(content).not_to match(/# javascript_transpiler:.*\n.*assets_bundler:/)
325+
# Should add after source_path instead (with single newline)
326+
expect(content).to match(/source_path:.*\n\n.*# Select assets bundler/)
327+
end
328+
329+
it "preserves inline comments when adding assets_bundler" do
330+
config_with_inline = <<~YAML
331+
default: &default
332+
source_path: app/javascript # Important path
333+
javascript_transpiler: babel # Keep this
334+
335+
development:
336+
<<: *default
337+
YAML
338+
File.write(config_path, config_with_inline)
339+
340+
switcher.switch_to("rspack")
341+
content = File.read(config_path)
342+
# Should preserve inline comments
343+
expect(content).to include("source_path: app/javascript # Important path")
344+
expect(content).to include("javascript_transpiler: swc # Keep this")
345+
end
346+
347+
it "handles config with blank lines after default" do
348+
config_with_blanks = <<~YAML
349+
default: &default
350+
351+
source_path: app/javascript
352+
javascript_transpiler: babel
353+
354+
development:
355+
<<: *default
356+
YAML
357+
File.write(config_path, config_with_blanks)
358+
359+
switcher.switch_to("rspack")
360+
config = load_yaml_for_test(config_path)
361+
expect(config["default"]["assets_bundler"]).to eq("rspack")
362+
end
363+
364+
it "handles config with multiple blank lines after default" do
365+
config_many_blanks = <<~YAML
366+
default: &default
367+
368+
369+
source_path: app/javascript
370+
javascript_transpiler: babel
371+
372+
development:
373+
<<: *default
374+
YAML
375+
File.write(config_path, config_many_blanks)
376+
377+
switcher.switch_to("rspack")
378+
config = load_yaml_for_test(config_path)
379+
expect(config["default"]["assets_bundler"]).to eq("rspack")
380+
end
381+
159382
context "when already using the target bundler" do
160383
it "does not reinstall deps when install_deps is false" do
161384
expect(switcher).not_to receive(:system)

0 commit comments

Comments
 (0)