Skip to content

Commit b5a0f26

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 <[email protected]>
1 parent 1fe5690 commit b5a0f26

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
@@ -1171,20 +1171,16 @@ def validate_server_bundle_path_sync(rails_bundle_path)
11711171
begin
11721172
webpack_content = File.read(webpack_config_path)
11731173

1174-
# Extract the path from webpack config
1175-
# Look for: path: require('path').resolve(__dirname, '../../ssr-generated')
1176-
path_regex = %r{path:\s*require\(['"]path['"]\)\.resolve\(__dirname,\s*['"]\.\./\.\./([^'"]+)['"]\)}
1177-
path_match = webpack_content.match(path_regex)
1178-
1179-
unless path_match
1180-
checker.add_info("\n ℹ️ Could not parse webpack server bundle path - skipping validation")
1181-
return
1182-
end
1174+
# Try to extract the path from webpack config
1175+
webpack_bundle_path = extract_webpack_output_path(webpack_content, webpack_config_path)
1176+
1177+
return unless webpack_bundle_path
11831178

1184-
webpack_bundle_path = path_match[1]
1179+
# Normalize and compare paths
1180+
normalized_webpack_path = normalize_path(webpack_bundle_path)
1181+
normalized_rails_path = normalize_path(rails_bundle_path)
11851182

1186-
# Compare the paths
1187-
if webpack_bundle_path == rails_bundle_path
1183+
if normalized_webpack_path == normalized_rails_path
11881184
checker.add_success("\n ✅ Webpack and Rails configs are in sync (both use '#{rails_bundle_path}')")
11891185
else
11901186
checker.add_warning(<<~MSG.strip)
@@ -1209,6 +1205,43 @@ def validate_server_bundle_path_sync(rails_bundle_path)
12091205
checker.add_info("\n ℹ️ Could not validate webpack config: #{e.message}")
12101206
end
12111207
end
1208+
1209+
# Extract output.path from webpack config, supporting multiple patterns
1210+
def extract_webpack_output_path(webpack_content, _webpack_config_path)
1211+
# Pattern 1: path: require('path').resolve(__dirname, '../../ssr-generated')
1212+
hardcoded_pattern = %r{path:\s*require\(['"]path['"]\)\.resolve\(__dirname,\s*['"]\.\./\.\./([^'"]+)['"]\)}
1213+
if (match = webpack_content.match(hardcoded_pattern))
1214+
return match[1]
1215+
end
1216+
1217+
# Pattern 2: path: config.outputPath (can't validate - runtime value)
1218+
if webpack_content.match?(/path:\s*config\.outputPath/)
1219+
checker.add_info(<<~MSG.strip)
1220+
\n ℹ️ Webpack config uses config.outputPath (from shakapacker.yml)
1221+
Cannot validate sync with Rails config as this is resolved at build time.
1222+
Ensure your shakapacker.yml public_output_path matches server_bundle_output_path.
1223+
MSG
1224+
return nil
1225+
end
1226+
1227+
# Pattern 3: path: some_variable (can't validate)
1228+
if webpack_content.match?(/path:\s*[a-zA-Z_]\w*/)
1229+
checker.add_info("\n ℹ️ Webpack config uses a variable for output.path - cannot validate")
1230+
return nil
1231+
end
1232+
1233+
checker.add_info("\n ℹ️ Could not parse webpack server bundle path - skipping validation")
1234+
nil
1235+
end
1236+
1237+
# Normalize path for comparison (remove leading ./, trailing /)
1238+
def normalize_path(path)
1239+
return path unless path.is_a?(String)
1240+
1241+
normalized = path.strip
1242+
normalized = normalized.sub(%r{^\.?/}, "") # Remove leading ./ or /
1243+
normalized.sub(%r{/$}, "") # Remove trailing /
1244+
end
12121245
end
12131246
# rubocop:enable Metrics/ClassLength
12141247
end

spec/lib/react_on_rails/doctor_spec.rb

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,4 +188,201 @@
188188
end
189189
end
190190
end
191+
192+
describe "server bundle path validation" do
193+
let(:doctor) { described_class.new }
194+
let(:checker) { doctor.instance_variable_get(:@checker) }
195+
196+
before do
197+
allow(checker).to receive(:add_info)
198+
allow(checker).to receive(:add_success)
199+
allow(checker).to receive(:add_warning)
200+
end
201+
202+
describe "#validate_server_bundle_path_sync" do
203+
context "when webpack config file doesn't exist" do
204+
before do
205+
allow(File).to receive(:exist?).with("config/webpack/serverWebpackConfig.js").and_return(false)
206+
end
207+
208+
it "adds info message and skips validation" do
209+
expected_msg = "\n ℹ️ Webpack server config not found - skipping path validation"
210+
expect(checker).to receive(:add_info).with(expected_msg)
211+
doctor.send(:validate_server_bundle_path_sync, "ssr-generated")
212+
end
213+
end
214+
215+
context "when webpack config uses hardcoded path" do
216+
let(:webpack_content) do
217+
<<~JS
218+
serverWebpackConfig.output = {
219+
filename: 'server-bundle.js',
220+
path: require('path').resolve(__dirname, '../../ssr-generated'),
221+
};
222+
JS
223+
end
224+
225+
before do
226+
allow(File).to receive(:exist?).with("config/webpack/serverWebpackConfig.js").and_return(true)
227+
allow(File).to receive(:read).with("config/webpack/serverWebpackConfig.js").and_return(webpack_content)
228+
end
229+
230+
it "reports success when paths match" do
231+
expected_msg = "\n ✅ Webpack and Rails configs are in sync (both use 'ssr-generated')"
232+
expect(checker).to receive(:add_success).with(expected_msg)
233+
doctor.send(:validate_server_bundle_path_sync, "ssr-generated")
234+
end
235+
236+
it "reports warning when paths don't match" do
237+
expect(checker).to receive(:add_warning).with(/Configuration mismatch detected/)
238+
doctor.send(:validate_server_bundle_path_sync, "server-bundles")
239+
end
240+
241+
it "includes both paths in warning when mismatched" do
242+
expect(checker).to receive(:add_warning) do |msg|
243+
expect(msg).to include('server_bundle_output_path = "server-bundles"')
244+
expect(msg).to include('output.path = "ssr-generated"')
245+
end
246+
doctor.send(:validate_server_bundle_path_sync, "server-bundles")
247+
end
248+
end
249+
250+
context "when webpack config uses config.outputPath" do
251+
let(:webpack_content) do
252+
<<~JS
253+
serverWebpackConfig.output = {
254+
filename: 'server-bundle.js',
255+
path: config.outputPath,
256+
};
257+
JS
258+
end
259+
260+
before do
261+
allow(File).to receive(:exist?).with("config/webpack/serverWebpackConfig.js").and_return(true)
262+
allow(File).to receive(:read).with("config/webpack/serverWebpackConfig.js").and_return(webpack_content)
263+
end
264+
265+
it "reports that it cannot validate" do
266+
expect(checker).to receive(:add_info).with(/Webpack config uses config\.outputPath/)
267+
doctor.send(:validate_server_bundle_path_sync, "ssr-generated")
268+
end
269+
270+
it "does not report success or warning" do
271+
expect(checker).not_to receive(:add_success)
272+
expect(checker).not_to receive(:add_warning)
273+
doctor.send(:validate_server_bundle_path_sync, "ssr-generated")
274+
end
275+
end
276+
277+
context "when webpack config uses a variable" do
278+
let(:webpack_content) do
279+
<<~JS
280+
const outputPath = calculatePath();
281+
serverWebpackConfig.output = {
282+
path: outputPath,
283+
};
284+
JS
285+
end
286+
287+
before do
288+
allow(File).to receive(:exist?).with("config/webpack/serverWebpackConfig.js").and_return(true)
289+
allow(File).to receive(:read).with("config/webpack/serverWebpackConfig.js").and_return(webpack_content)
290+
end
291+
292+
it "reports that it cannot validate" do
293+
expect(checker).to receive(:add_info).with(/Webpack config uses a variable/)
294+
doctor.send(:validate_server_bundle_path_sync, "ssr-generated")
295+
end
296+
end
297+
298+
context "when webpack config reading fails" do
299+
before do
300+
allow(File).to receive(:exist?).with("config/webpack/serverWebpackConfig.js").and_return(true)
301+
allow(File).to receive(:read).and_raise(StandardError, "Permission denied")
302+
end
303+
304+
it "handles error gracefully" do
305+
expect(checker).to receive(:add_info).with(/Could not validate webpack config: Permission denied/)
306+
doctor.send(:validate_server_bundle_path_sync, "ssr-generated")
307+
end
308+
end
309+
end
310+
311+
describe "#extract_webpack_output_path" do
312+
context "with hardcoded path pattern" do
313+
let(:webpack_content) do
314+
"path: require('path').resolve(__dirname, '../../my-bundle-dir')"
315+
end
316+
317+
it "extracts the path" do
318+
result = doctor.send(:extract_webpack_output_path, webpack_content, "config/webpack/test.js")
319+
expect(result).to eq("my-bundle-dir")
320+
end
321+
end
322+
323+
context "with config.outputPath" do
324+
let(:webpack_content) { "path: config.outputPath" }
325+
326+
it "returns nil and adds info message" do
327+
expect(checker).to receive(:add_info).with(/config\.outputPath/)
328+
result = doctor.send(:extract_webpack_output_path, webpack_content, "config/webpack/test.js")
329+
expect(result).to be_nil
330+
end
331+
end
332+
333+
context "with variable" do
334+
let(:webpack_content) { "path: myPath" }
335+
336+
it "returns nil and adds info message" do
337+
expect(checker).to receive(:add_info).with(/variable/)
338+
result = doctor.send(:extract_webpack_output_path, webpack_content, "config/webpack/test.js")
339+
expect(result).to be_nil
340+
end
341+
end
342+
343+
context "with unrecognized pattern" do
344+
let(:webpack_content) { "output: {}" }
345+
346+
it "returns nil and adds info message" do
347+
expect(checker).to receive(:add_info).with(/Could not parse/)
348+
result = doctor.send(:extract_webpack_output_path, webpack_content, "config/webpack/test.js")
349+
expect(result).to be_nil
350+
end
351+
end
352+
end
353+
354+
describe "#normalize_path" do
355+
it "removes leading ./" do
356+
expect(doctor.send(:normalize_path, "./ssr-generated")).to eq("ssr-generated")
357+
end
358+
359+
it "removes leading /" do
360+
expect(doctor.send(:normalize_path, "/ssr-generated")).to eq("ssr-generated")
361+
end
362+
363+
it "removes trailing /" do
364+
expect(doctor.send(:normalize_path, "ssr-generated/")).to eq("ssr-generated")
365+
end
366+
367+
it "handles paths with both leading and trailing slashes" do
368+
expect(doctor.send(:normalize_path, "./ssr-generated/")).to eq("ssr-generated")
369+
end
370+
371+
it "strips whitespace" do
372+
expect(doctor.send(:normalize_path, " ssr-generated ")).to eq("ssr-generated")
373+
end
374+
375+
it "returns unchanged path if already normalized" do
376+
expect(doctor.send(:normalize_path, "ssr-generated")).to eq("ssr-generated")
377+
end
378+
379+
it "handles nil gracefully" do
380+
expect(doctor.send(:normalize_path, nil)).to be_nil
381+
end
382+
383+
it "handles non-string values gracefully" do
384+
expect(doctor.send(:normalize_path, 123)).to eq(123)
385+
end
386+
end
387+
end
191388
end

0 commit comments

Comments
 (0)