Skip to content

Commit 02713bf

Browse files
justin808claude
andcommitted
Fix validation to support multiple webpack patterns and add comprehensive tests
Addresses critical issues identified in code review: **Issue 1: Regex Pattern Too Restrictive** - Original regex only matched hardcoded path.resolve() pattern - Real dummy app uses config.outputPath, which wasn't matched at all - Validation would silently skip without detecting actual configs **Fix:** - Support hardcoded path: require('path').resolve(__dirname, '../../path') - Detect config.outputPath usage (can't validate, inform user) - Detect variable usage (can't validate, inform user) - Clear messaging for each pattern type **Issue 2: Missing Path Normalization** - String equality comparison failed on equivalent paths - ./ssr-generated vs ssr-generated would be false mismatch - Trailing slashes caused false positives **Fix:** - normalize_path() method strips leading ./ and / - Removes trailing slashes - Handles whitespace - Graceful handling of nil/non-string values **Issue 3: No Test Coverage** - New validation methods had zero tests - High risk of regressions **Fix:** - Added 20+ comprehensive test cases covering: - File not found scenarios - Hardcoded paths (matching and mismatched) - config.outputPath detection - Variable detection - Error handling - Path normalization edge cases - All return nil vs success vs warning paths **Testing Results:** - All 20 new tests passing - 30/31 total specs passing (1 pre-existing failure unrelated) - Covers real-world patterns including dummy app config **Technical Details:** - extract_webpack_output_path() now pattern-aware - Better user messaging for unvalidatable configs - Maintains backward compatibility - No breaking changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 8a591dc commit 02713bf

File tree

2 files changed

+242
-12
lines changed

2 files changed

+242
-12
lines changed

lib/react_on_rails/doctor.rb

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1418,20 +1418,16 @@ def validate_server_bundle_path_sync(rails_bundle_path)
14181418
begin
14191419
webpack_content = File.read(webpack_config_path)
14201420

1421-
# Extract the path from webpack config
1422-
# Look for: path: require('path').resolve(__dirname, '../../ssr-generated')
1423-
path_regex = %r{path:\s*require\(['"]path['"]\)\.resolve\(__dirname,\s*['"]\.\./\.\./([^'"]+)['"]\)}
1424-
path_match = webpack_content.match(path_regex)
1425-
1426-
unless path_match
1427-
checker.add_info("\n ℹ️ Could not parse webpack server bundle path - skipping validation")
1428-
return
1429-
end
1421+
# Try to extract the path from webpack config
1422+
webpack_bundle_path = extract_webpack_output_path(webpack_content, webpack_config_path)
1423+
1424+
return unless webpack_bundle_path
14301425

1431-
webpack_bundle_path = path_match[1]
1426+
# Normalize and compare paths
1427+
normalized_webpack_path = normalize_path(webpack_bundle_path)
1428+
normalized_rails_path = normalize_path(rails_bundle_path)
14321429

1433-
# Compare the paths
1434-
if webpack_bundle_path == rails_bundle_path
1430+
if normalized_webpack_path == normalized_rails_path
14351431
checker.add_success("\n ✅ Webpack and Rails configs are in sync (both use '#{rails_bundle_path}')")
14361432
else
14371433
checker.add_warning(<<~MSG.strip)
@@ -1456,6 +1452,43 @@ def validate_server_bundle_path_sync(rails_bundle_path)
14561452
checker.add_info("\n ℹ️ Could not validate webpack config: #{e.message}")
14571453
end
14581454
end
1455+
1456+
# Extract output.path from webpack config, supporting multiple patterns
1457+
def extract_webpack_output_path(webpack_content, _webpack_config_path)
1458+
# Pattern 1: path: require('path').resolve(__dirname, '../../ssr-generated')
1459+
hardcoded_pattern = %r{path:\s*require\(['"]path['"]\)\.resolve\(__dirname,\s*['"]\.\./\.\./([^'"]+)['"]\)}
1460+
if (match = webpack_content.match(hardcoded_pattern))
1461+
return match[1]
1462+
end
1463+
1464+
# Pattern 2: path: config.outputPath (can't validate - runtime value)
1465+
if webpack_content.match?(/path:\s*config\.outputPath/)
1466+
checker.add_info(<<~MSG.strip)
1467+
\n ℹ️ Webpack config uses config.outputPath (from shakapacker.yml)
1468+
Cannot validate sync with Rails config as this is resolved at build time.
1469+
Ensure your shakapacker.yml public_output_path matches server_bundle_output_path.
1470+
MSG
1471+
return nil
1472+
end
1473+
1474+
# Pattern 3: path: some_variable (can't validate)
1475+
if webpack_content.match?(/path:\s*[a-zA-Z_]\w*/)
1476+
checker.add_info("\n ℹ️ Webpack config uses a variable for output.path - cannot validate")
1477+
return nil
1478+
end
1479+
1480+
checker.add_info("\n ℹ️ Could not parse webpack server bundle path - skipping validation")
1481+
nil
1482+
end
1483+
1484+
# Normalize path for comparison (remove leading ./, trailing /)
1485+
def normalize_path(path)
1486+
return path unless path.is_a?(String)
1487+
1488+
normalized = path.strip
1489+
normalized = normalized.sub(%r{^\.?/}, "") # Remove leading ./ or /
1490+
normalized.sub(%r{/$}, "") # Remove trailing /
1491+
end
14591492
end
14601493
# rubocop:enable Metrics/ClassLength
14611494
end

spec/lib/react_on_rails/doctor_spec.rb

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,4 +522,201 @@
522522
end
523523
end
524524
end
525+
526+
describe "server bundle path validation" do
527+
let(:doctor) { described_class.new }
528+
let(:checker) { doctor.instance_variable_get(:@checker) }
529+
530+
before do
531+
allow(checker).to receive(:add_info)
532+
allow(checker).to receive(:add_success)
533+
allow(checker).to receive(:add_warning)
534+
end
535+
536+
describe "#validate_server_bundle_path_sync" do
537+
context "when webpack config file doesn't exist" do
538+
before do
539+
allow(File).to receive(:exist?).with("config/webpack/serverWebpackConfig.js").and_return(false)
540+
end
541+
542+
it "adds info message and skips validation" do
543+
expected_msg = "\n ℹ️ Webpack server config not found - skipping path validation"
544+
expect(checker).to receive(:add_info).with(expected_msg)
545+
doctor.send(:validate_server_bundle_path_sync, "ssr-generated")
546+
end
547+
end
548+
549+
context "when webpack config uses hardcoded path" do
550+
let(:webpack_content) do
551+
<<~JS
552+
serverWebpackConfig.output = {
553+
filename: 'server-bundle.js',
554+
path: require('path').resolve(__dirname, '../../ssr-generated'),
555+
};
556+
JS
557+
end
558+
559+
before do
560+
allow(File).to receive(:exist?).with("config/webpack/serverWebpackConfig.js").and_return(true)
561+
allow(File).to receive(:read).with("config/webpack/serverWebpackConfig.js").and_return(webpack_content)
562+
end
563+
564+
it "reports success when paths match" do
565+
expected_msg = "\n ✅ Webpack and Rails configs are in sync (both use 'ssr-generated')"
566+
expect(checker).to receive(:add_success).with(expected_msg)
567+
doctor.send(:validate_server_bundle_path_sync, "ssr-generated")
568+
end
569+
570+
it "reports warning when paths don't match" do
571+
expect(checker).to receive(:add_warning).with(/Configuration mismatch detected/)
572+
doctor.send(:validate_server_bundle_path_sync, "server-bundles")
573+
end
574+
575+
it "includes both paths in warning when mismatched" do
576+
expect(checker).to receive(:add_warning) do |msg|
577+
expect(msg).to include('server_bundle_output_path = "server-bundles"')
578+
expect(msg).to include('output.path = "ssr-generated"')
579+
end
580+
doctor.send(:validate_server_bundle_path_sync, "server-bundles")
581+
end
582+
end
583+
584+
context "when webpack config uses config.outputPath" do
585+
let(:webpack_content) do
586+
<<~JS
587+
serverWebpackConfig.output = {
588+
filename: 'server-bundle.js',
589+
path: config.outputPath,
590+
};
591+
JS
592+
end
593+
594+
before do
595+
allow(File).to receive(:exist?).with("config/webpack/serverWebpackConfig.js").and_return(true)
596+
allow(File).to receive(:read).with("config/webpack/serverWebpackConfig.js").and_return(webpack_content)
597+
end
598+
599+
it "reports that it cannot validate" do
600+
expect(checker).to receive(:add_info).with(/Webpack config uses config\.outputPath/)
601+
doctor.send(:validate_server_bundle_path_sync, "ssr-generated")
602+
end
603+
604+
it "does not report success or warning" do
605+
expect(checker).not_to receive(:add_success)
606+
expect(checker).not_to receive(:add_warning)
607+
doctor.send(:validate_server_bundle_path_sync, "ssr-generated")
608+
end
609+
end
610+
611+
context "when webpack config uses a variable" do
612+
let(:webpack_content) do
613+
<<~JS
614+
const outputPath = calculatePath();
615+
serverWebpackConfig.output = {
616+
path: outputPath,
617+
};
618+
JS
619+
end
620+
621+
before do
622+
allow(File).to receive(:exist?).with("config/webpack/serverWebpackConfig.js").and_return(true)
623+
allow(File).to receive(:read).with("config/webpack/serverWebpackConfig.js").and_return(webpack_content)
624+
end
625+
626+
it "reports that it cannot validate" do
627+
expect(checker).to receive(:add_info).with(/Webpack config uses a variable/)
628+
doctor.send(:validate_server_bundle_path_sync, "ssr-generated")
629+
end
630+
end
631+
632+
context "when webpack config reading fails" do
633+
before do
634+
allow(File).to receive(:exist?).with("config/webpack/serverWebpackConfig.js").and_return(true)
635+
allow(File).to receive(:read).and_raise(StandardError, "Permission denied")
636+
end
637+
638+
it "handles error gracefully" do
639+
expect(checker).to receive(:add_info).with(/Could not validate webpack config: Permission denied/)
640+
doctor.send(:validate_server_bundle_path_sync, "ssr-generated")
641+
end
642+
end
643+
end
644+
645+
describe "#extract_webpack_output_path" do
646+
context "with hardcoded path pattern" do
647+
let(:webpack_content) do
648+
"path: require('path').resolve(__dirname, '../../my-bundle-dir')"
649+
end
650+
651+
it "extracts the path" do
652+
result = doctor.send(:extract_webpack_output_path, webpack_content, "config/webpack/test.js")
653+
expect(result).to eq("my-bundle-dir")
654+
end
655+
end
656+
657+
context "with config.outputPath" do
658+
let(:webpack_content) { "path: config.outputPath" }
659+
660+
it "returns nil and adds info message" do
661+
expect(checker).to receive(:add_info).with(/config\.outputPath/)
662+
result = doctor.send(:extract_webpack_output_path, webpack_content, "config/webpack/test.js")
663+
expect(result).to be_nil
664+
end
665+
end
666+
667+
context "with variable" do
668+
let(:webpack_content) { "path: myPath" }
669+
670+
it "returns nil and adds info message" do
671+
expect(checker).to receive(:add_info).with(/variable/)
672+
result = doctor.send(:extract_webpack_output_path, webpack_content, "config/webpack/test.js")
673+
expect(result).to be_nil
674+
end
675+
end
676+
677+
context "with unrecognized pattern" do
678+
let(:webpack_content) { "output: {}" }
679+
680+
it "returns nil and adds info message" do
681+
expect(checker).to receive(:add_info).with(/Could not parse/)
682+
result = doctor.send(:extract_webpack_output_path, webpack_content, "config/webpack/test.js")
683+
expect(result).to be_nil
684+
end
685+
end
686+
end
687+
688+
describe "#normalize_path" do
689+
it "removes leading ./" do
690+
expect(doctor.send(:normalize_path, "./ssr-generated")).to eq("ssr-generated")
691+
end
692+
693+
it "removes leading /" do
694+
expect(doctor.send(:normalize_path, "/ssr-generated")).to eq("ssr-generated")
695+
end
696+
697+
it "removes trailing /" do
698+
expect(doctor.send(:normalize_path, "ssr-generated/")).to eq("ssr-generated")
699+
end
700+
701+
it "handles paths with both leading and trailing slashes" do
702+
expect(doctor.send(:normalize_path, "./ssr-generated/")).to eq("ssr-generated")
703+
end
704+
705+
it "strips whitespace" do
706+
expect(doctor.send(:normalize_path, " ssr-generated ")).to eq("ssr-generated")
707+
end
708+
709+
it "returns unchanged path if already normalized" do
710+
expect(doctor.send(:normalize_path, "ssr-generated")).to eq("ssr-generated")
711+
end
712+
713+
it "handles nil gracefully" do
714+
expect(doctor.send(:normalize_path, nil)).to be_nil
715+
end
716+
717+
it "handles non-string values gracefully" do
718+
expect(doctor.send(:normalize_path, 123)).to eq(123)
719+
end
720+
end
721+
end
525722
end

0 commit comments

Comments
 (0)