Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Changes since the last non-beta release.
### Fixed

- **Fixed NODE_ENV=test causing DefinePlugin warnings**. [PR #870](https://github.com/shakacode/shakapacker/pull/870) by [justin808](https://github.com/justin808). When RAILS_ENV=test, Shakapacker now sets NODE_ENV=development instead of NODE_ENV=test. This prevents webpack/rspack DefinePlugin conflicts since these bundlers only recognize "development" and "production" as valid NODE_ENV values.
- **Fixed `--json` flag output being corrupted by log messages**. [PR #869](https://github.com/shakacode/shakapacker/pull/869) by [justin808](https://github.com/justin808). `[Shakapacker]` log messages are now written to stderr instead of stdout, allowing `bin/shakapacker --profile --json` to produce valid JSON output that can be piped to tools like `webpack-bundle-analyzer`. Resolves [#868](https://github.com/shakacode/shakapacker/issues/868).

## [v9.5.0] - January 7, 2026

Expand Down
8 changes: 4 additions & 4 deletions lib/shakapacker/dev_server_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ def self.run_with_build_config(argv, build_config)
# This ensures the bundler override (from --bundler or build config) is respected
ENV["SHAKAPACKER_ASSETS_BUNDLER"] = build_config[:bundler]

puts "[Shakapacker] Running dev server for build: #{build_config[:name]}"
puts "[Shakapacker] Description: #{build_config[:description]}" if build_config[:description]
puts "[Shakapacker] Bundler: #{build_config[:bundler]}"
puts "[Shakapacker] Config file: #{build_config[:config_file]}" if build_config[:config_file]
$stderr.puts "[Shakapacker] Running dev server for build: #{build_config[:name]}"
$stderr.puts "[Shakapacker] Description: #{build_config[:description]}" if build_config[:description]
$stderr.puts "[Shakapacker] Bundler: #{build_config[:bundler]}"
$stderr.puts "[Shakapacker] Config file: #{build_config[:config_file]}" if build_config[:config_file]

# Pass bundler override so Configuration.assets_bundler reflects the build
new(argv, build_config, build_config[:bundler]).run
Expand Down
46 changes: 23 additions & 23 deletions lib/shakapacker/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ def self.run(argv)

# If this build uses dev server, delegate to DevServerRunner
if loader.uses_dev_server?(build_config)
$stdout.puts "[Shakapacker] Build '#{build_name}' requires dev server"
$stdout.puts "[Shakapacker] Running: bin/shakapacker-dev-server --build #{build_name}"
$stdout.puts ""
$stderr.puts "[Shakapacker] Build '#{build_name}' requires dev server"
$stderr.puts "[Shakapacker] Running: bin/shakapacker-dev-server --build #{build_name}"
$stderr.puts ""
require_relative "dev_server_runner"
DevServerRunner.run_with_build_config(remaining_argv, build_config)
return
Expand Down Expand Up @@ -179,10 +179,10 @@ def self.run_with_build_config(argv, build_config)
# This ensures the bundler override (from --bundler or build config) is respected
ENV["SHAKAPACKER_ASSETS_BUNDLER"] = build_config[:bundler]

puts "[Shakapacker] Running build: #{build_config[:name]}"
puts "[Shakapacker] Description: #{build_config[:description]}" if build_config[:description]
puts "[Shakapacker] Bundler: #{build_config[:bundler]}"
puts "[Shakapacker] Config file: #{build_config[:config_file]}" if build_config[:config_file]
$stderr.puts "[Shakapacker] Running build: #{build_config[:name]}"
$stderr.puts "[Shakapacker] Description: #{build_config[:description]}" if build_config[:description]
$stderr.puts "[Shakapacker] Bundler: #{build_config[:bundler]}"
$stderr.puts "[Shakapacker] Config file: #{build_config[:config_file]}" if build_config[:config_file]

# Create runner with modified argv and bundler from build_config
# The build_config[:bundler] already has any CLI --bundler override applied
Expand Down Expand Up @@ -240,40 +240,40 @@ def package_json
end

def run
puts "[Shakapacker] Preparing environment for assets bundler execution..."
$stderr.puts "[Shakapacker] Preparing environment for assets bundler execution..."
env = Shakapacker::Compiler.env
env["SHAKAPACKER_CONFIG"] = @shakapacker_config
env["NODE_OPTIONS"] = ENV["NODE_OPTIONS"] || ""

cmd = build_cmd
puts "[Shakapacker] Base command: #{cmd.join(" ")}"
$stderr.puts "[Shakapacker] Base command: #{cmd.join(" ")}"

if @argv.delete("--debug-shakapacker")
puts "[Shakapacker] Debug mode enabled (--debug-shakapacker)"
$stderr.puts "[Shakapacker] Debug mode enabled (--debug-shakapacker)"
env["NODE_OPTIONS"] = "#{env["NODE_OPTIONS"]} --inspect-brk"
end

if @argv.delete "--trace-deprecation"
puts "[Shakapacker] Trace deprecation enabled (--trace-deprecation)"
$stderr.puts "[Shakapacker] Trace deprecation enabled (--trace-deprecation)"
env["NODE_OPTIONS"] = "#{env["NODE_OPTIONS"]} --trace-deprecation"
end

if @argv.delete "--no-deprecation"
puts "[Shakapacker] Deprecation warnings disabled (--no-deprecation)"
$stderr.puts "[Shakapacker] Deprecation warnings disabled (--no-deprecation)"
env["NODE_OPTIONS"] = "#{env["NODE_OPTIONS"]} --no-deprecation"
end

# Commands are not compatible with --config option.
if (@argv & assets_bundler_commands).empty?
puts "[Shakapacker] Adding config file: #{@webpack_config}"
$stderr.puts "[Shakapacker] Adding config file: #{@webpack_config}"
cmd += ["--config", @webpack_config]
else
puts "[Shakapacker] Skipping config file (running assets bundler command: #{(@argv & assets_bundler_commands).join(", ")})"
$stderr.puts "[Shakapacker] Skipping config file (running assets bundler command: #{(@argv & assets_bundler_commands).join(", ")})"
end

cmd += @argv
puts "[Shakapacker] Final command: #{cmd.join(" ")}"
puts "[Shakapacker] Working directory: #{@app_path}"
$stderr.puts "[Shakapacker] Final command: #{cmd.join(" ")}"
$stderr.puts "[Shakapacker] Working directory: #{@app_path}"

watch_mode = @argv.include?("--watch") || @argv.include?("-w")
start_time = Time.now unless watch_mode
Expand All @@ -288,7 +288,7 @@ def run
minutes = (elapsed_time / 60).floor
seconds = (elapsed_time % 60).round(2)
time_display = minutes > 0 ? "#{minutes}:#{format('%05.2f', seconds)}s" : "#{elapsed_time.round(2)}s"
puts "[Shakapacker] Completed #{bundler_name} build in #{time_display} (#{elapsed_time.round(2)}s)"
$stderr.puts "[Shakapacker] Completed #{bundler_name} build in #{time_display} (#{elapsed_time.round(2)}s)"
end
exit($?.exitstatus || 1) unless $?.success?
end
Expand Down Expand Up @@ -601,10 +601,10 @@ def find_rspack_config_with_fallback
File.join(@app_path, config_dir, "rspack.config.#{ext}")
end

puts "[Shakapacker] Looking for Rspack config in: #{rspack_paths.join(", ")}"
$stderr.puts "[Shakapacker] Looking for Rspack config in: #{rspack_paths.join(", ")}"
rspack_path = rspack_paths.find { |f| File.exist?(f) }
if rspack_path
puts "[Shakapacker] Found Rspack config: #{rspack_path}"
$stderr.puts "[Shakapacker] Found Rspack config: #{rspack_path}"
return rspack_path
end

Expand All @@ -613,7 +613,7 @@ def find_rspack_config_with_fallback
File.join(@app_path, config_dir, "webpack.config.#{ext}")
end

puts "[Shakapacker] Rspack config not found, checking for webpack config fallback..."
$stderr.puts "[Shakapacker] Rspack config not found, checking for webpack config fallback..."
webpack_path = webpack_paths.find { |f| File.exist?(f) }
if webpack_path
$stderr.puts "⚠️ DEPRECATION WARNING: Using webpack config file for Rspack assets bundler."
Expand All @@ -629,7 +629,7 @@ def find_rspack_config_with_fallback
File.join(@app_path, "config/webpack", "webpack.config.#{ext}")
end

puts "[Shakapacker] Checking config/webpack/ for backward compatibility..."
$stderr.puts "[Shakapacker] Checking config/webpack/ for backward compatibility..."
webpack_dir_path = webpack_dir_paths.find { |f| File.exist?(f) }
if webpack_dir_path
$stderr.puts "⚠️ DEPRECATION WARNING: Found webpack config in config/webpack/ but assets_bundler is set to 'rspack'."
Expand All @@ -656,13 +656,13 @@ def find_webpack_config
possible_paths = %w[ts js].map do |ext|
File.join(@app_path, config_dir, "webpack.config.#{ext}")
end
puts "[Shakapacker] Looking for Webpack config in: #{possible_paths.join(", ")}"
$stderr.puts "[Shakapacker] Looking for Webpack config in: #{possible_paths.join(", ")}"
path = possible_paths.find { |f| File.exist?(f) }
unless path
print_config_not_found_error("webpack", possible_paths.last, config_dir)
exit(1)
end
puts "[Shakapacker] Found Webpack config: #{path}"
$stderr.puts "[Shakapacker] Found Webpack config: #{path}"
path
end
end
Expand Down
16 changes: 8 additions & 8 deletions spec/shakapacker/rspack_runner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@
true
end

output = capture_stdout { klass.run([]) }
output = capture_stderr { klass.run([]) }

# The test app may have webpack config, so bundler name could be either
# Time format can be either "X.XXs" or "M:SS.SSs" for the display, always "X.XXs" in parentheses
Expand All @@ -164,7 +164,7 @@
true
end

output = capture_stdout { klass.run(["--watch"]) }
output = capture_stderr { klass.run(["--watch"]) }

expect(output).not_to match(/Completed (webpack|rspack) build/)
end
Expand All @@ -183,7 +183,7 @@
true
end

output = capture_stdout { klass.run(["-w"]) }
output = capture_stderr { klass.run(["-w"]) }

expect(output).not_to match(/Completed (webpack|rspack) build/)
end
Expand All @@ -192,13 +192,13 @@

private

def capture_stdout
old_stdout = $stdout
$stdout = StringIO.new
def capture_stderr
old_stderr = $stderr
$stderr = StringIO.new
yield
$stdout.string
$stderr.string
ensure
$stdout = old_stdout
$stderr = old_stderr
end

def verify_command(cmd, argv: [])
Expand Down
21 changes: 6 additions & 15 deletions spec/shakapacker/runner_build_config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
allow(klass).to receive(:new).and_return(instance)
allow(instance).to receive(:system).and_return(true)

output = capture_stdout do
output = capture_stderr do
klass.run(["--build", "prod"])
end

Expand Down Expand Up @@ -85,7 +85,7 @@
allow(dev_server_klass).to receive(:new).and_return(dev_server_instance)
allow(dev_server_instance).to receive(:run).and_return(nil)

output = capture_stdout do
output = capture_stderr do
klass.run(["--build", "dev-hmr"])
end

Expand Down Expand Up @@ -114,7 +114,7 @@
allow(klass).to receive(:new).and_return(instance)
allow(instance).to receive(:system).and_return(true)

output = capture_stdout do
output = capture_stderr do
klass.run(["nonexistent"])
end

Expand All @@ -132,7 +132,7 @@
allow(klass).to receive(:new).and_return(instance)
allow(instance).to receive(:system).and_return(true)

output = capture_stdout do
output = capture_stderr do
klass.run([])
end

Expand Down Expand Up @@ -198,7 +198,7 @@
allow(klass).to receive(:new).and_return(instance)
allow(instance).to receive(:run).and_return(nil)

output = capture_stdout do
output = capture_stderr do
klass.run(["--build", "dev"])
end

Expand Down Expand Up @@ -264,7 +264,7 @@
allow(dev_server_klass).to receive(:new).and_return(dev_server_instance)
allow(dev_server_instance).to receive(:run).and_return(nil)

output = capture_stdout do
output = capture_stderr do
klass.run(["--build", "dev-hmr"])
end

Expand All @@ -277,15 +277,6 @@

private

def capture_stdout
old_stdout = $stdout
$stdout = StringIO.new
yield
$stdout.string
ensure
$stdout = old_stdout
end

def capture_stderr
old_stderr = $stderr
$stderr = StringIO.new
Expand Down
68 changes: 63 additions & 5 deletions spec/shakapacker/webpack_runner_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@
true
end

output = capture_stdout { klass.run([]) }
output = capture_stderr { klass.run([]) }

# Time format can be either "X.XXs" or "M:SS.SSs" for the display, always "X.XXs" in parentheses
expect(output).to match(/\[Shakapacker\] Completed webpack build in (\d+:\d+\.\d+s|\d+\.\d+s) \(\d+\.\d+s\)/)
Expand All @@ -178,7 +178,7 @@
true
end

output = capture_stdout { klass.run(["--watch"]) }
output = capture_stderr { klass.run(["--watch"]) }

expect(output).not_to match(/Completed webpack build/)
end
Expand All @@ -197,7 +197,7 @@
true
end

output = capture_stdout { klass.run([]) }
output = capture_stderr { klass.run([]) }

# Should show format like "3.29s (3.29s)" without minutes
expect(output).to match(/\[Shakapacker\] Completed webpack build in \d+\.\d+s \(\d+\.\d+s\)/)
Expand All @@ -206,15 +206,73 @@
end
end

describe "stdout/stderr separation for JSON output" do
it "does not write [Shakapacker] log messages to stdout" do
Dir.chdir(test_app_path) do
klass = Shakapacker::WebpackRunner
instance = klass.new(["--json"])

allow(klass).to receive(:new).and_return(instance)
allow(Shakapacker::Utils::Manager).to receive(:error_unless_package_manager_is_obvious!)

allow(instance).to receive(:system) do |*args|
system("true")
true
end

stdout_output, stderr_output = capture_stdout_and_stderr { klass.run(["--json"]) }

# Stdout should NOT contain [Shakapacker] log messages
expect(stdout_output).not_to match(/\[Shakapacker\]/)

# Stderr SHOULD contain [Shakapacker] log messages
expect(stderr_output).to match(/\[Shakapacker\]/)
end
end

it "keeps stdout clean for valid JSON output when using --json flag" do
Dir.chdir(test_app_path) do
klass = Shakapacker::WebpackRunner
instance = klass.new(["--profile", "--json"])

allow(klass).to receive(:new).and_return(instance)
allow(Shakapacker::Utils::Manager).to receive(:error_unless_package_manager_is_obvious!)

allow(instance).to receive(:system) do |*args|
system("true")
true
end

stdout_output, = capture_stdout_and_stderr { klass.run(["--profile", "--json"]) }

# Stdout should be empty (no log messages polluting it)
# The actual JSON would come from webpack itself, not shakapacker
expect(stdout_output).to be_empty
end
end
end

private

def capture_stdout
def capture_stdout_and_stderr
old_stdout = $stdout
old_stderr = $stderr
$stdout = StringIO.new
$stderr = StringIO.new
yield
$stdout.string
[$stdout.string, $stderr.string]
ensure
$stdout = old_stdout
$stderr = old_stderr
end

def capture_stderr
old_stderr = $stderr
$stderr = StringIO.new
yield
$stderr.string
ensure
$stderr = old_stderr
end

def verify_command(cmd, argv: [])
Expand Down
Loading