Skip to content

Conversation

@justin808
Copy link
Member

@justin808 justin808 commented Oct 9, 2025

Summary

Implements Phase 1 of #667: Core Diff Engine for comparing webpack/rspack configurations.

This PR adds a comprehensive configuration comparison tool with contextual information that helps developers understand not just WHAT changed, but WHY it matters.

Example Output

Contextual Format (default)

Shows what each change means with documentation and impact analysis:

================================================================================
Webpack/Rspack Configuration Comparison
================================================================================

Comparing: webpack-development-client.yaml
      vs:  webpack-production-client.yaml

Found 4 difference(s): 1 added, 0 removed, 3 changed

================================================================================

1. [~] mode

   What it does:
   Defines the environment mode (development, production, or none).
   Controls built-in optimizations and defaults.

   Affects: Minification, tree-shaking, source maps, and performance optimizations

   Old value: "development"
   New value: "production"

   Impact: Enabling production optimizations (minification, tree-shaking)

   Documentation: https://webpack.js.org/configuration/mode/

2. [~] devtool

   What it does:
   Controls how source maps are generated for debugging.
   Different values trade off between build speed and debugging quality.

   Affects: Build speed, debugging experience, and bundle size

   Old value: "cheap-module-source-map"
   New value: "source-map"

   Impact: Full source maps - best debugging but slower builds

   Documentation: https://webpack.js.org/configuration/devtool/

3. [~] optimization.minimize

   What it does:
   Enable/disable minification of JavaScript bundles.

   Affects: Bundle size and build time

   Default: true in production, false in development

   Old value: false
   New value: true

   Impact: Code will be minified - smaller bundles but slower builds

   Documentation: https://webpack.js.org/configuration/optimization/#optimizationminimize

4. [+] optimization.minimizer

   What it does:
   Array of plugins to use for minification.

   Affects: Minification strategy and bundle size

   Default: TerserPlugin for JS

   Added value: "[Array(1)]"

   Documentation: https://webpack.js.org/configuration/optimization/#optimizationminimizer

================================================================================

Legend:
  [+] = Added in right config
  [-] = Removed from right config
  [~] = Changed between configs

Summary Format

Concise one-liner for CI or quick checks:

$ bin/diff-bundler-config --left=dev.yaml --right=prod.yaml --format=summary

8 changes: +3 -1 ~4

JSON Format (for CI/CD)

Machine-readable output for automated processing:

{
  "summary": {
    "totalChanges": 8,
    "added": 3,
    "removed": 1,
    "changed": 4
  },
  "entries": [
    {
      "operation": "changed",
      "path": {
        "path": ["mode"],
        "humanPath": "mode"
      },
      "oldValue": "development",
      "newValue": "production",
      "valueType": "string"
    }
  ]
}

Why Use This Instead of Visual Diff Tools?

Configuration-Specific Intelligence

  • Understands webpack/rspack structure (nested objects, arrays, plugins)
  • Provides context: "This enables production optimizations"
  • Links to official documentation
  • Shows default values and impact

Path Normalization

  • Automatically normalizes absolute paths to relative paths
  • Makes diffs portable: /Users/you/app and /home/ci/app both become ./app
  • Prevents false positives from different machine paths

Focused Analysis

  • Filter out noise with --ignore-paths and --ignore-keys
  • Example: --ignore-paths="plugins.*" to focus on core config
  • Wildcard support for flexible filtering

Multiple Output Formats

  • Contextual: Full explanations with docs (default)
  • Summary: One-line counts for CI
  • JSON/YAML: Machine-readable for automation

Semantic Understanding

  • Explains WHAT the key does
  • Shows WHY it matters (what it affects)
  • Analyzes IMPACT of the change
  • Traditional diff tools just show line changes

Integration with --doctor Mode

The --doctor mode exports all configs in one command, optimized for comparison:

# Step 1: Export everything
bin/export-bundler-config --doctor

# This creates:
# - webpack-development-client.yaml
# - webpack-development-server.yaml
# - webpack-production-client.yaml
# - webpack-production-server.yaml

# Step 2: Compare any combination
bin/diff-bundler-config \
  --left=shakapacker-config-exports/webpack-development-client.yaml \
  --right=shakapacker-config-exports/webpack-production-client.yaml

Benefits:

  • All configs exported in one command
  • Consistent YAML format optimized for comparison
  • Includes metadata about when/how configs were generated
  • Perfect for attaching to support requests or bug reports

Common Troubleshooting Workflows

"Works in Dev, Fails in Prod"

bin/export-bundler-config --doctor
bin/diff-bundler-config \
  --left=shakapacker-config-exports/webpack-development-client.yaml \
  --right=shakapacker-config-exports/webpack-production-client.yaml

The contextual output will explain each difference and its impact.

After Upgrading Dependencies

# Before upgrade
bin/export-bundler-config --save --output=before.yaml

# After upgrade
bin/export-bundler-config --save --output=after.yaml

bin/diff-bundler-config --left=before.yaml --right=after.yaml

See exactly what changed with documentation links to understand new defaults.

Webpack → Rspack Migration

bin/export-bundler-config --bundler=webpack --save --output=webpack.yaml
rake shakapacker:switch_bundler
bin/export-bundler-config --bundler=rspack --save --output=rspack.yaml

bin/diff-bundler-config \
  --left=webpack.yaml \
  --right=rspack.yaml \
  --ignore-paths="plugins.*"

Focus on core config differences with contextual explanations.

Key Features

Core Diff Engine

  • Deep object comparison with path-level granularity
  • Handles nested objects, arrays, and special types (functions, RegExp, Date)
  • Accurate path tracking using human-readable dot notation
  • Configurable comparison depth and filtering options

Configuration Documentation

  • 20+ documented webpack/rspack keys with explanations
  • What each key does, what it affects, and default values
  • Links to official webpack documentation
  • Impact analysis for common changes

Path Normalization

  • Automatic normalization of absolute paths to relative paths
  • Each side detects its own base path independently
  • Excludes URLs (webpack://, http://) and module specifiers (@scope/package)
  • Can be disabled with --no-normalize-paths

Multiple Output Formats

  • Contextual: Full explanations with docs and impact (default)
  • Summary: One-line counts (8 changes: +3 -1 ~4)
  • JSON: Machine-readable for CI/CD
  • YAML: Structured format for documentation

Advanced Options

  • Ignore keys: Filter out specific keys across all levels
  • Ignore paths: Skip specific configuration paths with wildcard support
  • Include unchanged: Optionally show unchanged values
  • Max depth: Limit comparison depth for large configs

CLI Command

# Compare with contextual information (default)
bin/diff-bundler-config \
  --left=config1.yaml \
  --right=config2.yaml

# Quick summary
bin/diff-bundler-config \
  --left=dev.yaml \
  --right=prod.yaml \
  --format=summary

# Generate JSON report for CI
bin/diff-bundler-config \
  --left=baseline.json \
  --right=current.json \
  --format=json \
  --output=diff-report.json

# Ignore specific paths with wildcards
bin/diff-bundler-config \
  --left=config1.yaml \
  --right=config2.yaml \
  --ignore-paths="plugins.*,output.path"

Supported File Formats

  • JSON (.json)
  • YAML (.yaml, .yml)
  • JavaScript (.js)
  • TypeScript (.ts) - requires ts-node

Test Coverage

Comprehensive test suite with 39 passing tests covering:

  • Primitive value comparisons
  • Nested object and array diffing
  • Special type handling (functions, RegExp, Date)
  • All configuration options
  • Path normalization logic (excluding URLs and module specifiers)
  • All output formatters

Documentation

  • Complete guide in docs/config-diff.md with 5 troubleshooting workflows
  • Updated README.md with quick start examples
  • Comprehensive help text in CLI
  • "Why use this instead of visual diff?" section explaining advantages

Code Quality

All review feedback addressed:

  • ✅ Separate basePath detection for left/right configs
  • ✅ Removed no-op map in path formatting
  • ✅ Fixed path detection to exclude URLs and module specifiers
  • ✅ Replaced generic formats with contextual information
  • ✅ Added configuration documentation database

Testing

All tests pass:

  • ✅ 172 Jest tests (including 39 diff engine tests)
  • ✅ 773 RSpec tests
  • ✅ ESLint
  • ✅ RuboCop
  • ✅ TypeScript compilation

Next Steps

This implements Phase 1 of the roadmap in #667. Future phases will add:

  • Phase 2: Baseline generation via presets
  • Phase 3: Integrated "export + diff" workflow
  • Phase 4: Enhanced semantic analysis
  • Phase 5: Specialized webpack → rspack migration support

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a configuration diff tool/CLI to compare bundler configs with summary, detailed, JSON, and YAML outputs; supports path normalization, ignore/depth controls, and file or stdout output; includes per-entry contextual detail and impact guidance.
  • Documentation

    • Added a comprehensive Configuration Diff Guide and README troubleshooting subsection with examples, quick-start usage, supported formats, and advanced options.
  • Tests

    • Added unit tests covering diffing logic, formatting outputs, and path normalization.
  • Chores

    • Added CLI shims, package export entry, and example dev/prod config samples.

@coderabbitai
Copy link

coderabbitai bot commented Oct 9, 2025

Walkthrough

Adds a new Configuration Diff tool: CLI and shell shims, DiffEngine, DiffFormatter, PathNormalizer, config docs/types, package export, README/docs additions, example configs, and unit tests for comparing bundler configurations.

Changes

Cohort / File(s) Summary
Documentation
README.md, docs/config-diff.md
Adds "Comparing Configurations" to README and a comprehensive docs/config-diff.md guide with quick start, usage, options, formats, examples, exit codes, and programmatic usage.
CLI & Executables
CLI: package/configDiffer/cli.ts
Shims: bin/diff-bundler-config, lib/install/bin/diff-bundler-config
New CLI run(args: string[]): Promise<number> and Node shims that load left/right configs (json
Core Library & API
package/configDiffer/diffEngine.ts, package/configDiffer/formatter.ts, package/configDiffer/pathNormalizer.ts, package/configDiffer/configDocs.ts, package/configDiffer/types.ts, package/configDiffer/index.ts
Adds DiffEngine (deep structural diff), DiffFormatter (json/yaml/contextual/summary/detailed with docs/impact hints), PathNormalizer (normalize/detectBasePath), config docs map and helpers, TypeScript types, and consolidated exports.
Package Export
package.json
Adds package export entry "./configDiffer": "./package/configDiffer/index.js".
Tests
test/configDiffer/diffEngine.test.js, test/configDiffer/formatter.test.js, test/configDiffer/pathNormalizer.test.js
New unit tests covering DiffEngine (primitives, objects, arrays, special types, options), DiffFormatter outputs (JSON/YAML/summary/detailed), and PathNormalizer normalize/detectBasePath behavior.
Examples
tmp/examples/dev-config.yaml, tmp/examples/prod-config.yaml
Adds sample development and production YAML configs used in docs/examples.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant CLI as diff-bundler-config (CLI)
  participant Loader as Config Loader
  participant PN as PathNormalizer
  participant DE as DiffEngine
  participant DF as DiffFormatter
  participant Output as STDOUT/File

  User->>CLI: invoke --left <L> --right <R> [options]
  CLI->>Loader: load L and R (json|yaml|js|ts)
  alt normalize paths
    CLI->>PN: detectBasePath()/normalize(configs)
    PN-->>CLI: normalized configs
  end
  CLI->>DE: compare(left,right,options)
  DE-->>CLI: DiffResult (entries, summary, metadata)
  CLI->>DF: format(result, format)
  DF-->>CLI: formatted string
  alt --output given
    CLI->>Output: write file
  else
    CLI->>Output: print to STDOUT
  end
  CLI-->>User: exit code (0 no diffs/help, 1 diffs/errors)
  opt Error
    CLI-->>User: print error to STDERR and exit 1
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Files/areas to inspect closely:

  • package/configDiffer/diffEngine.ts — traversal correctness, ignorePatterns/wildcards, depth handling, and path formatting.
  • package/configDiffer/formatter.ts — formatting edge cases, impact analysis, documentation lookups, and large value handling.
  • package/configDiffer/cli.ts and shims — JS/TS input loading, error handling, exit codes, and file I/O.
  • package/configDiffer/pathNormalizer.ts — base path detection, cross-platform path handling, and path detection heuristics.
  • Tests — ensure assertions match intended behavior and cover edge cases.

Suggested reviewers

  • tomdracz

Poem

A rabbit hops through configs with delight,
Comparing keys by moon and daylight.
Old values bow, new values gleam,
YAML and JSON join the team.
Hop to the diff — the trail is seen! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "Add configuration diff engine - Phase 1 of #667" accurately and directly summarizes the main change in the changeset. The PR introduces a complete configuration diff engine system with core modules (DiffEngine, DiffFormatter, PathNormalizer), CLI tooling (bin/diff-bundler-config), multiple output formats, configuration documentation, and comprehensive tests—all of which align with the stated objective. The title is concise, specific, and avoids vague or generic language, making it immediately clear to teammates reviewing commit history that this PR adds a new configuration comparison feature tied to issue #667.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch config-diff-engine

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link

claude bot commented Oct 9, 2025

Code Review: Configuration Diff Engine - Phase 1

I've completed a thorough review of this PR. Overall, this is a well-implemented, well-tested, and well-documented feature.

Strengths

Excellent Code Quality

  • Clean, modular architecture with clear separation of concerns
  • TypeScript types are well-defined with proper interfaces
  • Code is readable and maintainable

Comprehensive Test Coverage

  • 39 new tests covering all major functionality
  • Tests cover edge cases: primitives, nested objects, arrays, special types
  • All configuration options are tested

Great Documentation

  • Excellent README additions with clear examples
  • Comprehensive docs/config-diff.md with multiple usage scenarios
  • Clear CLI help text

User Experience

  • Multiple output formats provide flexibility
  • Path normalization makes diffs more readable
  • Exit codes enable CI/CD integration

Areas for Improvement

1. Potential Bugs

CLI Error Handling (cli.ts:167-176): If ts-node is not available, it throws an error but still tries to require the TypeScript file on line 176. Should return early after throwing.

Missing File Validation: loadConfigFile doesn't validate that the loaded config is actually an object/array before processing.

2. Performance Considerations

Deep Cloning (pathNormalizer.ts:11-16): Creates a deep copy of entire config, could be memory-intensive for very large configs. Consider documenting memory considerations.

Array Comparison (diffEngine.ts:115-126): Index-based comparison means reordering items shows as many changes. Should document this behavior.

3. Code Quality Suggestions

Missing Trailing Newlines: Per CLAUDE.md, check bin/diff-bundler-config and lib/install/bin/diff-bundler-config

Type Safety (cli.ts:133): No validation that parseInt succeeded. Invalid input becomes NaN.

Error Handling (diffEngine.ts:173-178): Wildcard regex matching doesn't handle invalid patterns.

4. Testing Gaps

Consider adding: CLI integration tests, large config tests, error cases (invalid formats, malformed JSON/YAML, missing files), edge cases (empty configs, deeply nested objects)

5. Documentation Enhancements

  • Memory/performance guidance for large configs
  • Array comparison behavior
  • More wildcard syntax examples

Recommendations

High Priority

  1. Fix TypeScript file loading error handling
  2. Add config validation after loading files
  3. Add input validation for maxDepth parsing
  4. Ensure all files have trailing newlines

Medium Priority

  1. Add CLI integration tests
  2. Document array comparison behavior
  3. Add error handling for invalid wildcard patterns

Security Assessment

Overall: Low Risk. This is a dev tool operating on local files. Dynamic require is acceptable for this use case. Consider adding a note in docs about running only trusted config files.

Summary

Code Quality: 5/5
Test Coverage: 4/5
Documentation: 5/5
Performance: 4/5
Security: 4/5
User Experience: 5/5

Overall: 4.5/5

Approval Status

This PR is approved with minor suggestions. The issues identified are minor and can be addressed in follow-up PRs. The core functionality is solid, well-tested, and ready for production use.

Great work on this feature! The modular design will make Phase 2-5 implementations much easier.

Reviewed with Claude Code

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 91781d1 and b4a6d06.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (16)
  • README.md (1 hunks)
  • bin/diff-bundler-config (1 hunks)
  • docs/config-diff.md (1 hunks)
  • lib/install/bin/diff-bundler-config (1 hunks)
  • package.json (1 hunks)
  • package/configDiffer/cli.ts (1 hunks)
  • package/configDiffer/diffEngine.ts (1 hunks)
  • package/configDiffer/formatter.ts (1 hunks)
  • package/configDiffer/index.ts (1 hunks)
  • package/configDiffer/pathNormalizer.ts (1 hunks)
  • package/configDiffer/types.ts (1 hunks)
  • test/configDiffer/diffEngine.test.js (1 hunks)
  • test/configDiffer/formatter.test.js (1 hunks)
  • test/configDiffer/pathNormalizer.test.js (1 hunks)
  • tmp/examples/dev-config.yaml (1 hunks)
  • tmp/examples/prod-config.yaml (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-28T02:23:21.827Z
Learnt from: CR
PR: shakacode/shakapacker#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-28T02:23:21.827Z
Learning: This gem supports both webpack and rspack configurations

Applied to files:

  • README.md
🧬 Code graph analysis (7)
test/configDiffer/formatter.test.js (2)
package/configDiffer/formatter.ts (1)
  • DiffFormatter (4-162)
package/configDiffer/index.ts (1)
  • DiffFormatter (3-3)
test/configDiffer/pathNormalizer.test.js (2)
package/configDiffer/index.ts (1)
  • PathNormalizer (4-4)
package/configDiffer/pathNormalizer.ts (1)
  • PathNormalizer (4-143)
test/configDiffer/diffEngine.test.js (1)
package/configDiffer/diffEngine.ts (1)
  • DiffEngine (9-306)
package/configDiffer/pathNormalizer.ts (2)
package/configDiffer/index.ts (2)
  • PathNormalizer (4-4)
  • NormalizedConfig (11-11)
package/configDiffer/types.ts (1)
  • NormalizedConfig (44-48)
package/configDiffer/diffEngine.ts (1)
package/configDiffer/types.ts (5)
  • DiffOptions (34-42)
  • DiffEntry (8-14)
  • DiffResult (16-32)
  • DiffOperation (1-1)
  • DiffPath (3-6)
package/configDiffer/cli.ts (4)
package/configDiffer/types.ts (1)
  • DiffOptions (34-42)
package/configDiffer/pathNormalizer.ts (1)
  • PathNormalizer (4-143)
package/configDiffer/diffEngine.ts (1)
  • DiffEngine (9-306)
package/configDiffer/formatter.ts (1)
  • DiffFormatter (4-162)
package/configDiffer/formatter.ts (1)
package/configDiffer/types.ts (3)
  • DiffResult (16-32)
  • DiffEntry (8-14)
  • DiffOperation (1-1)
🪛 markdownlint-cli2 (0.18.1)
docs/config-diff.md

9-9: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


15-15: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


21-21: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


27-27: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


34-34: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


329-329: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (23)
package.json (1)

25-25: LGTM! Export follows established conventions.

The new configDiffer export is consistent with the existing export pattern (like configExporter on line 24) and properly exposes the new diff tooling functionality.

tmp/examples/dev-config.yaml (1)

1-18: LGTM! Well-structured development configuration example.

This example config provides a realistic development setup that pairs nicely with the production config for demonstrating the diff tool's capabilities. The configuration choices (cheap-module-source-map, minimize: false, hot reloading) are appropriate for development.

tmp/examples/prod-config.yaml (1)

1-19: LGTM! Production configuration demonstrates key differences effectively.

This production config provides excellent contrast with the development config, highlighting important differences like contenthash fingerprinting, minification, TerserPlugin, and CompressionPlugin. The pair of configs effectively demonstrates the diff tool's value.

package/configDiffer/types.ts (1)

1-48: LGTM! Comprehensive and well-structured type definitions.

The type system is well-designed with:

  • Clear operation types using string literal unions
  • Hierarchical structure (DiffPath, DiffEntry, DiffResult)
  • Flexible DiffOptions supporting various use cases
  • Appropriate use of any for handling arbitrary configuration objects

The optional properties and metadata structure provide good extensibility for future enhancements.

bin/diff-bundler-config (1)

1-11: LGTM! Clean shim with proper error handling.

The implementation follows best practices:

  • Minimal shim delegates to TypeScript module
  • Correctly passes CLI arguments via process.argv.slice(2)
  • Proper promise handling with explicit exit codes
  • Errors printed to stderr before exit

The pattern is consistent with the rest of the codebase and provides a clean separation between the executable entry point and the implementation logic.

README.md (1)

791-816: LGTM! Clear and helpful documentation.

The new section provides:

  • Practical examples showing real-world usage
  • Multiple output format options (detailed, summary, JSON, YAML)
  • Clear use cases that demonstrate the tool's value
  • Logical placement after the "Debugging Configuration" section
  • Reference to the detailed Configuration Diff Guide for comprehensive information

The documentation effectively guides users from basic usage to understanding when and why to use the tool.

lib/install/bin/diff-bundler-config (1)

1-11: LGTM! Install shim mirrors bin implementation correctly.

The duplication between this file and bin/diff-bundler-config is intentional and follows the established pattern in this codebase for providing installation-time shims. The identical implementation ensures consistent behavior regardless of how the tool is accessed.

test/configDiffer/pathNormalizer.test.js (1)

1-129: LGTM! Comprehensive test coverage for PathNormalizer.

The test suite provides excellent coverage:

  • normalize() tests cover various scenarios: absolute paths, non-paths, nested structures, arrays, paths outside base, and relative paths
  • detectBasePath() tests handle multiple paths, absence of paths, and single-path scenarios
  • Clear, descriptive test names make the suite maintainable
  • Good balance of happy-path and edge-case testing

The tests effectively validate the path normalization functionality that's central to the diff engine's ability to compare configurations across different environments.

package/configDiffer/diffEngine.ts (9)

13-23: LGTM! Well-structured initialization.

The constructor properly sets sensible defaults for all options using nullish coalescing, making the API ergonomic for consumers.


25-39: LGTM! Clean public API.

The compare method correctly resets state, performs the diff, and returns a well-structured result with metadata including timestamp.


41-96: LGTM! Solid recursive comparison logic.

The traversal correctly handles:

  • Path ignoring and max depth constraints
  • Undefined values (added/removed)
  • Primitive comparisons with equality checks
  • Array and object recursion
  • Fallback for edge cases

The logic flow is clear and comprehensive.


98-128: LGTM! Array and object comparison is correct.

Both methods properly:

  • Enumerate all keys/indices
  • Skip ignored keys
  • Build paths accurately ([i] for arrays, key names for objects)
  • Recurse with incremented depth

130-153: LGTM! Entry creation and path formatting.

The methods correctly serialize values, format paths with both array and human-readable representations, and build entries with appropriate metadata.


184-224: Good type guards with appropriate equality checks.

The type guards correctly classify values, and areEqual handles special cases for functions, RegExp, and Date. Note that comparing functions via toString() (line 212) is fragile—it can produce false positives if functions differ only in formatting or variable names. However, for a configuration diff tool, this is likely acceptable since config functions are typically small and stable.


226-267: LGTM! Type inference and value serialization.

Both methods correctly handle special types and provide appropriate string representations for display. The function length threshold (200 chars) and string truncation are reasonable heuristics.


269-305: LGTM! Summary calculation is correct.

The method properly aggregates counts by operation type, excludes unchanged from totalChanges, and conditionally includes the unchanged property based on options.

Minor note: Line 292's || 0 is unnecessary since unchanged is initialized to 0 on line 281, but it's harmless.


170-182: Ignore bracket-notation concern. createHumanPath joins all segments with the configured separator (default “.”), so an array segment “[0]” becomes “foo.[0]”, and startsWith("foo.") correctly matches.

Likely an incorrect or invalid review comment.

package/configDiffer/formatter.ts (6)

1-7: LGTM! Clean JSON formatting.

Simple and effective—uses standard JSON.stringify with 2-space indentation for readability.


9-22: LGTM! Proper YAML formatting with correct js-yaml API.

The YAML formatter:

  • Uses dump (the safe, recommended API in js-yaml 4.x)
  • Groups entries by operation for structured output
  • Configures appropriate options (noRefs prevents circular reference markers, sortKeys: false preserves insertion order)

Based on learnings


24-47: LGTM! Clear summary output.

The summary formatter provides a concise, readable overview with:

  • Prominent section headers
  • Aligned operation counts
  • Special handling for zero changes
  • Optional unchanged count

49-106: LGTM! Comprehensive detailed output.

The detailed formatter builds a full report with:

  • Metadata (timestamp, file paths)
  • Embedded summary
  • Grouped changes by operation (added, removed, changed)
  • Per-entry formatting with clear section markers

The structure is well-organized and human-readable.


108-144: LGTM! Entry and value formatting.

Both methods correctly:

  • Format entry details (path, type, values) with consistent indentation
  • Handle operation-specific value display (added shows newValue, removed shows oldValue, changed shows both)
  • Truncate long strings at 100 chars for readability
  • Handle primitive types and fall back to JSON.stringify

146-161: LGTM! Operation grouping logic.

The method correctly initializes all operation types and populates them via iteration, ensuring the return type matches Record<DiffOperation, DiffEntry[]>.

@justin808
Copy link
Member Author

Updated based on feedback

Code Review Fixes

  1. Fixed basePath detection - Each config now detects its own base path independently for more accurate normalization
  2. Removed no-op map in createHumanPath - cleaner code
  3. Fixed path detection - Now correctly excludes URLs (webpack://, http://) and module specifiers (@scope/package) from normalization

Format Improvements

Replaced the generic summary/detailed formats with a contextual format that provides real value:

Before (useless):

➕ ADDED (1)
Path: optimization.minimize
Type: boolean  
Value: true

After (useful):

4. [~] optimization.minimize

   What it does:
   Enable/disable minification of JavaScript bundles.

   Affects: Bundle size and build time

   Default: true in production, false in development

   Old value: false
   New value: true

   Impact: Code will be minified - smaller bundles but slower builds

   Documentation: https://webpack.js.org/configuration/optimization/#optimizationminimize

New Features

  • Configuration documentation database with 20+ webpack/rspack keys documented
  • Impact analysis that explains what the change means (e.g., "Enabling production optimizations")
  • Documentation links to official webpack docs
  • Default values shown where applicable
  • Concise summary format: 8 changes: +3 -1 ~4 instead of verbose table

Why This is Better

The new format answers:

  • What is this key? - Clear explanation
  • Why should I care? - Shows what it affects
  • What's the impact? - Explains consequences of the change
  • Where can I learn more? - Links to official docs

This transforms the diff from "here's what changed" into "here's what changed AND what it means for your app."

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (7)
package/configDiffer/diffEngine.ts (3)

177-187: Functions and RegExp classified as primitives may cause confusion.

The isPrimitive method treats functions and RegExp instances as primitives. While this makes sense for the diff logic (they're compared by string representation), it's semantically unusual and could be confusing for maintainers. Consider renaming to shouldCompareByValue or adding a comment explaining the rationale.

Add a clarifying comment:

 private isPrimitive(value: any): boolean {
+  // Note: Functions and RegExp are treated as "primitives" because they're
+  // compared by their string representation rather than recursively
   return (
     value === null ||
     typeof value === "string" ||
     typeof value === "number" ||
     typeof value === "boolean" ||
     typeof value === "undefined" ||
     typeof value === "function" ||
     value instanceof RegExp
   )
 }

199-217: Function equality comparison may produce false negatives.

Comparing functions via toString() can fail for functions with different formatting but identical behavior (e.g., minified vs non-minified). This is acceptable for config comparison but could be noted in documentation.

Consider adding a comment to clarify the limitations:

 private areEqual(left: any, right: any): boolean {
   if (left === right) {
     return true
   }

+  // Note: Function comparison uses toString(), which is sensitive to formatting
+  // but sufficient for detecting meaningful config changes
   if (typeof left === "function" && typeof right === "function") {
     return left.toString() === right.toString()
   }

229-260: Potential information disclosure in function serialization.

When a function's string representation exceeds 200 characters, the code shows the character count. This could inadvertently reveal information about complex internal logic. Consider whether this level of detail is necessary or if a simpler representation would suffice.

Consider simplifying the output:

   if (typeof value === "function") {
     const fnStr = value.toString()
-    if (fnStr.length > 200) {
-      return `[Function: ${value.name || "anonymous"}] (${fnStr.length} chars)`
-    }
-    return `[Function: ${value.name || "anonymous"}]`
+    return `[Function: ${value.name || "anonymous"}]`
   }
package/configDiffer/cli.ts (1)

172-181: TypeScript support requires ts-node as peer dependency.

The code attempts to require ts-node/register/transpile-only but doesn't verify the version or show alternative installation methods. Users might install incompatible versions or be confused about whether it's a dev or peer dependency.

Consider improving the error message with more context:

     if (ext === ".ts") {
       try {
         require("ts-node/register/transpile-only")
       } catch (error) {
         throw new Error(
           "TypeScript config detected but ts-node is not available. " +
-            "Install ts-node as a dev dependency: npm install --save-dev ts-node"
+            "Install ts-node as a dev dependency: npm install --save-dev ts-node\n" +
+            "Alternatively, export your TypeScript config as JSON or YAML."
         )
       }
     }
package/configDiffer/formatter.ts (3)

167-173: Type coercion in boolean comparison may cause issues.

Line 168 checks newVal === true || newVal === "true", which handles both boolean and string representations. However, this may not catch other truthy values like 1 or "1" that could appear in configs. Consider normalizing to boolean first.

     if (path === "optimization.minimize") {
-      if (newVal === true || newVal === "true") {
+      const isEnabled = newVal === true || newVal === "true" || newVal === 1 || newVal === "1"
+      if (isEnabled) {
         return "Code will be minified - smaller bundles but slower builds"
       } else {
         return "Minification disabled - larger bundles but faster builds"
       }
     }

180-186: String type coercion could throw on non-string values.

Lines 181-182 call String(newVal).includes() which could produce unexpected results if newVal is null, undefined, or an object. While it won't throw, it might give false positives (e.g., String(null) returns "null" which could match if someone has a devtool option called "nulleval").

Add type checking:

       } else if (
-        String(newVal).includes("cheap") ||
-        String(newVal).includes("eval")
+        (typeof newVal === "string" && (newVal.includes("cheap") || newVal.includes("eval")))
       ) {
         return "Faster builds with some debugging capability"
       }

215-230: Type assertion bypasses type safety.

Line 229 uses a type assertion as Record<DiffOperation, DiffEntry[]> after manually constructing the grouped object. While this works, the manual initialization on lines 218-223 is fragile - if new operation types are added to DiffOperation, this function would silently fail to include them.

Consider a more type-safe approach:

   private groupByOperation(
     entries: DiffEntry[]
   ): Record<DiffOperation, DiffEntry[]> {
-    const grouped: Record<string, DiffEntry[]> = {
-      added: [],
-      removed: [],
-      changed: [],
-      unchanged: []
-    }
+    const operations: DiffOperation[] = ["added", "removed", "changed", "unchanged"]
+    const grouped: Record<DiffOperation, DiffEntry[]> = {} as Record<DiffOperation, DiffEntry[]>
+    
+    for (const op of operations) {
+      grouped[op] = []
+    }

     for (const entry of entries) {
       grouped[entry.operation].push(entry)
     }

-    return grouped as Record<DiffOperation, DiffEntry[]>
+    return grouped
   }

Alternatively, use a reduce for a more functional approach.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b4a6d06 and 646241d.

📒 Files selected for processing (6)
  • package/configDiffer/cli.ts (1 hunks)
  • package/configDiffer/configDocs.ts (1 hunks)
  • package/configDiffer/diffEngine.ts (1 hunks)
  • package/configDiffer/formatter.ts (1 hunks)
  • package/configDiffer/pathNormalizer.ts (1 hunks)
  • test/configDiffer/formatter.test.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • package/configDiffer/pathNormalizer.ts
  • test/configDiffer/formatter.test.js
🧰 Additional context used
🧬 Code graph analysis (3)
package/configDiffer/diffEngine.ts (1)
package/configDiffer/types.ts (5)
  • DiffOptions (34-42)
  • DiffEntry (8-14)
  • DiffResult (16-32)
  • DiffOperation (1-1)
  • DiffPath (3-6)
package/configDiffer/cli.ts (4)
package/configDiffer/types.ts (1)
  • DiffOptions (34-42)
package/configDiffer/pathNormalizer.ts (1)
  • PathNormalizer (4-167)
package/configDiffer/diffEngine.ts (1)
  • DiffEngine (9-299)
package/configDiffer/formatter.ts (1)
  • DiffFormatter (5-231)
package/configDiffer/formatter.ts (2)
package/configDiffer/types.ts (3)
  • DiffResult (16-32)
  • DiffEntry (8-14)
  • DiffOperation (1-1)
package/configDiffer/configDocs.ts (1)
  • getDocForKey (161-163)
🔇 Additional comments (8)
package/configDiffer/configDocs.ts (3)

4-9: LGTM! Clean interface definition.

The ConfigDoc interface is well-structured with appropriate optional fields for documentation metadata.


161-167: LGTM! Simple and effective helper functions.

The getDocForKey and hasDocumentation functions provide straightforward access to the documentation map.


11-159: Documentation URLs verified: All webpack documentation links return HTTP 200 and are valid.

package/configDiffer/diffEngine.ts (2)

9-23: LGTM! Constructor properly initializes options with sensible defaults.

The options are correctly typed as Required<DiffOptions> and all optional fields are handled with nullish coalescing, ensuring consistent behavior.


25-39: LGTM! Clean comparison entry point with proper state management.

The compare method correctly resets the entries array before each comparison and includes metadata with timestamps.

package/configDiffer/cli.ts (1)

16-89: LGTM! Previous review comment addressed - path normalization now uses separate base paths.

The run function correctly normalizes each configuration with its own detected base path (lines 38-49), addressing the previous review concern about cross-environment path normalization.

package/configDiffer/formatter.ts (2)

1-8: LGTM! Clean imports and simple JSON formatter.

The formatJson method provides a straightforward JSON representation with proper indentation.


10-23: LGTM! YAML formatting uses safe defaults.

The YAML formatter uses js-yaml with appropriate options: noRefs: true prevents circular reference issues, sortKeys: false preserves order, and lineWidth: 120 ensures readability.

Based on learnings: js-yaml v4.x uses safe-by-default dump, which is correctly used here.

Comment on lines +76 to +82
if (options.output) {
const fs = require("fs")
fs.writeFileSync(options.output, output, "utf8")
console.log(`Diff written to: ${options.output}`)
} else {
console.log(output)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Avoid dynamic require() inside conditional block.

The require('fs') call on line 77 is redundant since fs is already imported at the top of the file. This also creates inconsistency - other parts of the code use the named import writeFileSync would be cleaner.

Use the existing import:

+import { existsSync, readFileSync, writeFileSync } from "fs"
 import { resolve, extname } from "path"
 import { load as loadYaml } from "js-yaml"
 
 ...
 
     if (options.output) {
-      const fs = require("fs")
-      fs.writeFileSync(options.output, output, "utf8")
+      writeFileSync(options.output, output, "utf8")
       console.log(`Diff written to: ${options.output}`)
     } else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (options.output) {
const fs = require("fs")
fs.writeFileSync(options.output, output, "utf8")
console.log(`Diff written to: ${options.output}`)
} else {
console.log(output)
}
// At the top of package/configDiffer/cli.ts
import { existsSync, readFileSync, writeFileSync } from "fs"
import { resolve, extname } from "path"
import { load as loadYaml } from "js-yaml"
if (options.output) {
writeFileSync(options.output, output, "utf8")
console.log(`Diff written to: ${options.output}`)
} else {
console.log(output)
}
🤖 Prompt for AI Agents
In package/configDiffer/cli.ts around lines 76 to 82, remove the dynamic
require("fs") and the local fs variable and instead use the existing top-level
fs (or the named writeFileSync import) already imported at the top of the file;
replace fs.writeFileSync(options.output, output, "utf8") with the existing
writeFileSync(...) call (or fs.writeFileSync if using the fs default import) and
keep the console.log message unchanged.

Comment on lines +183 to +190
delete require.cache[resolvedPath]
let loaded = require(resolvedPath)

if (typeof loaded === "object" && "default" in loaded) {
loaded = loaded.default
}

return loaded
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Require cache clearing may not work for ES modules.

Line 183 clears the require cache to support reloading configs, but this only works for CommonJS modules. If the loaded JS/TS file uses ES modules, this won't have the expected effect and could lead to stale config being used on subsequent runs.

Document this limitation or add detection:

     delete require.cache[resolvedPath]
     let loaded = require(resolvedPath)

+    // Note: Cache clearing only works for CommonJS modules.
+    // For ES modules, configs may not reload properly in the same process.
+
     if (typeof loaded === "object" && "default" in loaded) {
       loaded = loaded.default
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
delete require.cache[resolvedPath]
let loaded = require(resolvedPath)
if (typeof loaded === "object" && "default" in loaded) {
loaded = loaded.default
}
return loaded
delete require.cache[resolvedPath]
let loaded = require(resolvedPath)
// Note: Cache clearing only works for CommonJS modules.
// For ES modules, configs may not reload properly in the same process.
if (typeof loaded === "object" && "default" in loaded) {
loaded = loaded.default
}
return loaded

Comment on lines +163 to +175
private shouldIgnorePath(path: string[]): boolean {
const humanPath = this.createHumanPath(path)
return this.options.ignorePaths.some((ignorePath) => {
if (ignorePath.includes("*")) {
const escapedPattern = ignorePath
.replace(/\./g, "\\.")
.replace(/\*/g, ".*")
const pattern = new RegExp("^" + escapedPattern + "$")
return pattern.test(humanPath)
}
return humanPath === ignorePath || humanPath.startsWith(ignorePath + ".")
})
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential regex denial-of-service (ReDoS) vulnerability.

The wildcard pattern conversion creates a regex with .* which can lead to catastrophic backtracking on certain inputs. If a user provides a malicious ignore pattern with nested wildcards or very long paths, this could cause the comparison to hang.

Consider adding a timeout or using a simpler matching algorithm:

 private shouldIgnorePath(path: string[]): boolean {
   const humanPath = this.createHumanPath(path)
   return this.options.ignorePaths.some((ignorePath) => {
     if (ignorePath.includes("*")) {
-      const escapedPattern = ignorePath
-        .replace(/\./g, "\\.")
-        .replace(/\*/g, ".*")
-      const pattern = new RegExp("^" + escapedPattern + "$")
-      return pattern.test(humanPath)
+      // Use a simpler glob-like matching to avoid ReDoS
+      const parts = ignorePath.split("*")
+      let position = 0
+      for (let i = 0; i < parts.length; i++) {
+        const part = parts[i].replace(/\./g, "\\.")
+        if (part.length > 0) {
+          const index = humanPath.indexOf(part, position)
+          if (index === -1) return false
+          if (i === 0 && index !== 0) return false
+          position = index + part.length
+        }
+      }
+      if (parts.length > 0 && parts[parts.length - 1].length > 0) {
+        return humanPath.endsWith(parts[parts.length - 1])
+      }
+      return true
     }
     return humanPath === ignorePath || humanPath.startsWith(ignorePath + ".")
   })
 }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +188 to +250
if (path.includes("output.filename")) {
if (String(newVal).includes("[contenthash]")) {
return "Cache busting enabled - better long-term caching"
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Path.includes() check could match unintended paths.

Line 188 uses path.includes("output.filename") which will match any path containing that substring, not just the exact key. For example, it would match "some.output.filename.custom" or "output.filename.template".

Use exact or prefix matching:

-    if (path.includes("output.filename")) {
+    if (path === "output.filename" || path.startsWith("output.filename.")) {
       if (String(newVal).includes("[contenthash]")) {
         return "Cache busting enabled - better long-term caching"
       }
     }
🤖 Prompt for AI Agents
In package/configDiffer/formatter.ts around lines 188 to 192, the
path.includes("output.filename") check can match unintended keys (any path
containing that substring); replace it with a precise match such as path ===
"output.filename" or a proper prefix/segment check (e.g., split on '.' and
compare the relevant segment or use startsWith("output.filename.") when matching
nested keys) so only the exact key or intended prefix triggers the cache-busting
message; update the conditional accordingly and keep the inner contenthash check
as-is.

@justin808
Copy link
Member Author

Further improvements based on feedback

Removed redundant information

  • ✅ Removed value type display (was redundant)
  • ✅ Replaced confusing "old/new" labels with meaningful file-based labels

New label extraction

The formatter now intelligently extracts short names from filenames:

  • webpack-development-client.yamldev-client
  • webpack-production-server.yamlprod-server
  • config1.yamlconfig1
  • Falls back to left/right if no pattern matches

Improved output format

Before:

Old value: "development"
New value: "production"

After:

Values:
  dev-client:  "development"
  prod-client: "production"

Full example

3. [~] mode

   What it does:
   Defines the environment mode (development, production, or none).
   Controls built-in optimizations and defaults.

   Affects: Minification, tree-shaking, source maps, and performance optimizations

   Values:
     dev:  "development"
     prod: "production"

   Impact: Enabling production optimizations (minification, tree-shaking)

   Documentation: https://webpack.js.org/configuration/mode/

Much clearer! You can immediately see which config has which value.

Future-proof for N-way comparison

This format naturally extends to comparing 3+ configs:

Values:
  dev-client:  "development"
  dev-server:  "development"  
  prod-client: "production"
  prod-server: "production"

The design is ready for when we want to add multi-file comparison support.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
package/configDiffer/formatter.ts (1)

246-250: Match output.filename precisely for impact hints.

The includes check catches any path containing that substring (e.g., optimization.output.filenameTemplate), leading to misleading cache-busting advice. Compare the path segments directly instead:

-    if (path.includes("output.filename")) {
+    const segments = entry.path.path
+    if (
+      segments.length >= 2 &&
+      segments[0] === "output" &&
+      segments[1] === "filename"
+    ) {
package/configDiffer/cli.ts (2)

183-190: Document require cache limitation for ES modules.

The require.cache clearing on line 183 only works for CommonJS modules. ES modules loaded in the same process won't reload, potentially causing stale config to be used.

Add a comment documenting this limitation:

     delete require.cache[resolvedPath]
+    // Note: Cache clearing only works for CommonJS modules.
+    // For ES modules, configs may not reload properly in the same process.
     let loaded = require(resolvedPath)

     if (typeof loaded === "object" && "default" in loaded) {

1-1: Add writeFileSync to import and remove dynamic require.

The dynamic require('fs') on line 77 is inconsistent with the top-level import on line 1. To resolve this, add writeFileSync to the named imports and use it directly.

Apply this diff:

-import { existsSync, readFileSync } from "fs"
+import { existsSync, readFileSync, writeFileSync } from "fs"
 import { resolve, extname } from "path"
 import { load as loadYaml } from "js-yaml"

Then remove the dynamic require:

     if (options.output) {
-      const fs = require("fs")
-      fs.writeFileSync(options.output, output, "utf8")
+      writeFileSync(options.output, output, "utf8")
       console.log(`Diff written to: ${options.output}`)
     } else {

Also applies to: 76-79

🧹 Nitpick comments (1)
docs/config-diff.md (1)

329-351: Annotate the command reference code block.

Please specify a language (e.g., text or bash) for this fenced block so markdownlint stops flagging MD040 and downstream renderers get proper formatting.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d27583 and 8ad0017.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (17)
  • README.md (1 hunks)
  • bin/diff-bundler-config (1 hunks)
  • docs/config-diff.md (1 hunks)
  • lib/install/bin/diff-bundler-config (1 hunks)
  • package.json (1 hunks)
  • package/configDiffer/cli.ts (1 hunks)
  • package/configDiffer/configDocs.ts (1 hunks)
  • package/configDiffer/diffEngine.ts (1 hunks)
  • package/configDiffer/formatter.ts (1 hunks)
  • package/configDiffer/index.ts (1 hunks)
  • package/configDiffer/pathNormalizer.ts (1 hunks)
  • package/configDiffer/types.ts (1 hunks)
  • test/configDiffer/diffEngine.test.js (1 hunks)
  • test/configDiffer/formatter.test.js (1 hunks)
  • test/configDiffer/pathNormalizer.test.js (1 hunks)
  • tmp/examples/dev-config.yaml (1 hunks)
  • tmp/examples/prod-config.yaml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (9)
  • package/configDiffer/configDocs.ts
  • tmp/examples/dev-config.yaml
  • bin/diff-bundler-config
  • test/configDiffer/diffEngine.test.js
  • package/configDiffer/index.ts
  • lib/install/bin/diff-bundler-config
  • test/configDiffer/formatter.test.js
  • package.json
  • package/configDiffer/types.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

End every file with a trailing newline character

Files:

  • package/configDiffer/formatter.ts
  • package/configDiffer/diffEngine.ts
  • README.md
  • package/configDiffer/cli.ts
  • test/configDiffer/pathNormalizer.test.js
  • tmp/examples/prod-config.yaml
  • package/configDiffer/pathNormalizer.ts
  • docs/config-diff.md
🧬 Code graph analysis (5)
package/configDiffer/formatter.ts (2)
package/configDiffer/types.ts (3)
  • DiffResult (16-32)
  • DiffEntry (8-14)
  • DiffOperation (1-1)
package/configDiffer/configDocs.ts (1)
  • getDocForKey (161-163)
package/configDiffer/diffEngine.ts (1)
package/configDiffer/types.ts (5)
  • DiffOptions (34-42)
  • DiffEntry (8-14)
  • DiffResult (16-32)
  • DiffOperation (1-1)
  • DiffPath (3-6)
package/configDiffer/cli.ts (4)
package/configDiffer/types.ts (1)
  • DiffOptions (34-42)
package/configDiffer/pathNormalizer.ts (1)
  • PathNormalizer (4-167)
package/configDiffer/diffEngine.ts (1)
  • DiffEngine (9-299)
package/configDiffer/formatter.ts (1)
  • DiffFormatter (5-289)
test/configDiffer/pathNormalizer.test.js (1)
package/configDiffer/pathNormalizer.ts (1)
  • PathNormalizer (4-167)
package/configDiffer/pathNormalizer.ts (2)
package/configDiffer/index.ts (2)
  • PathNormalizer (4-4)
  • NormalizedConfig (11-11)
package/configDiffer/types.ts (1)
  • NormalizedConfig (44-48)
🪛 markdownlint-cli2 (0.18.1)
docs/config-diff.md

9-9: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


15-15: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


21-21: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


27-27: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


34-34: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


329-329: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (11)
test/configDiffer/pathNormalizer.test.js (2)

3-90: LGTM!

The normalize test suite provides comprehensive coverage of the PathNormalizer's core functionality: absolute-to-relative conversion, non-path preservation, nested object/array handling, and edge cases. The test structure is clear and validates expected behavior effectively.


92-128: LGTM!

The detectBasePath test suite effectively validates the static method's behavior across key scenarios: deriving common prefixes from multiple paths, handling configs without paths, and single-path configs. The assertions appropriately account for platform path differences.

package/configDiffer/cli.ts (2)

38-49: LGTM!

The path normalization correctly uses independent base paths for left and right configs, addressing the past review concern about cross-environment path differences. Each config now normalizes paths relative to its own detected base, ensuring consistent relative paths across machines.


91-155: LGTM!

The CLI implementation is robust:

  • parseArguments comprehensively handles all flags with proper validation and clear error messages
  • loadConfigFile supports multiple formats (JSON, YAML, JS, TS) with appropriate error handling and ts-node integration
  • showHelp provides detailed usage documentation with practical examples and exit code descriptions

The implementation follows best practices for CLI design.

Also applies to: 157-182, 191-260

package/configDiffer/pathNormalizer.ts (7)

4-17: LGTM!

The constructor and normalize method are well-designed. The optional basePath parameter with a sensible default (process.cwd()) provides flexibility, and the returned NormalizedConfig structure preserves both original and normalized configs along with the base path used.


19-39: LGTM!

The normalizeValue method correctly handles recursive normalization of nested structures. The use of isPlainObject prevents incorrect traversal of special types (Date, RegExp, functions) while properly normalizing strings, arrays, and plain objects.


41-55: LGTM!

The normalizePath method implements correct path normalization logic:

  • Early return for non-path strings avoids unnecessary processing
  • Converts paths to relative form with "./" prefix for consistency
  • Preserves paths outside the base path (starting with "..") unchanged
  • Normalizes separators to "/" for cross-platform consistency

57-99: LGTM!

The looksLikePath method now correctly restricts path detection to actual filesystem paths, addressing the past review concern. The implementation:

  • Excludes URLs with schemes (http://, https://, webpack://, etc.) via regex on line 63
  • Excludes module specifiers starting with @ on line 68
  • Only matches filesystem path patterns: absolute paths, relative prefixes (./, ../), home paths (~/, and Windows drive paths

This prevents false positives on URLs and module specifiers, ensuring only legitimate filesystem paths are normalized.


101-119: LGTM!

The isPlainObject method correctly identifies plain objects by excluding null, primitives, arrays, Date, RegExp, and functions. This ensures special types aren't incorrectly traversed during normalization.


121-142: LGTM!

The detectBasePath static method correctly extracts absolute paths from configs and derives a common base path. Returning undefined when no paths exist allows callers to fall back to process.cwd(), providing sensible behavior for configs without filesystem paths.


144-166: LGTM!

The findCommonPrefix static method correctly handles edge cases and derives the common directory prefix:

  • Empty array returns empty string
  • Single path returns its directory (filename removed)
  • Multiple paths find the longest common prefix by comparing path segments

The implementation is robust and handles cross-platform separators correctly.

Comment on lines +51 to +96
if (this.options.maxDepth !== null && depth > this.options.maxDepth) {
return
}

const leftType = this.getValueType(left)
const rightType = this.getValueType(right)

if (left === undefined && right === undefined) {
return
}

if (left === undefined) {
this.addEntry("added", path, undefined, right, rightType)
return
}

if (right === undefined) {
this.addEntry("removed", path, left, undefined, leftType)
return
}

if (this.isPrimitive(left) || this.isPrimitive(right)) {
if (!this.areEqual(left, right)) {
this.addEntry("changed", path, left, right, leftType)
} else if (this.options.includeUnchanged) {
this.addEntry("unchanged", path, left, right, leftType)
}
return
}

if (Array.isArray(left) && Array.isArray(right)) {
this.compareArrays(left, right, path, depth)
return
}

if (this.isObject(left) && this.isObject(right)) {
this.compareObjects(left, right, path, depth)
return
}

if (!this.areEqual(left, right)) {
this.addEntry("changed", path, left, right, leftType)
} else if (this.options.includeUnchanged) {
this.addEntry("unchanged", path, left, right, leftType)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Honor maxDepth without hiding differences.

When maxDepth is reached (e.g., value 0 or 1), the early return prevents any entry from being recorded, so configurations can appear identical even though deeper nodes differ. We still need an aggregated added/removed/changed (or unchanged) entry at the current path before exiting. Apply this fix:

-    if (this.options.maxDepth !== null && depth > this.options.maxDepth) {
-      return
-    }
-
-    const leftType = this.getValueType(left)
-    const rightType = this.getValueType(right)
+    const leftType = this.getValueType(left)
+    const rightType = this.getValueType(right)
+
+    if (this.options.maxDepth !== null && depth > this.options.maxDepth) {
+      if (left === undefined && right !== undefined) {
+        this.addEntry("added", path, undefined, right, rightType)
+      } else if (right === undefined && left !== undefined) {
+        this.addEntry("removed", path, left, undefined, leftType)
+      } else if (!this.areEqual(left, right)) {
+        this.addEntry("changed", path, left, right, leftType)
+      } else if (this.options.includeUnchanged) {
+        this.addEntry("unchanged", path, left, right, leftType)
+      }
+      return
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (this.options.maxDepth !== null && depth > this.options.maxDepth) {
return
}
const leftType = this.getValueType(left)
const rightType = this.getValueType(right)
if (left === undefined && right === undefined) {
return
}
if (left === undefined) {
this.addEntry("added", path, undefined, right, rightType)
return
}
if (right === undefined) {
this.addEntry("removed", path, left, undefined, leftType)
return
}
if (this.isPrimitive(left) || this.isPrimitive(right)) {
if (!this.areEqual(left, right)) {
this.addEntry("changed", path, left, right, leftType)
} else if (this.options.includeUnchanged) {
this.addEntry("unchanged", path, left, right, leftType)
}
return
}
if (Array.isArray(left) && Array.isArray(right)) {
this.compareArrays(left, right, path, depth)
return
}
if (this.isObject(left) && this.isObject(right)) {
this.compareObjects(left, right, path, depth)
return
}
if (!this.areEqual(left, right)) {
this.addEntry("changed", path, left, right, leftType)
} else if (this.options.includeUnchanged) {
this.addEntry("unchanged", path, left, right, leftType)
}
}
const leftType = this.getValueType(left)
const rightType = this.getValueType(right)
if (this.options.maxDepth !== null && depth > this.options.maxDepth) {
if (left === undefined && right !== undefined) {
this.addEntry("added", path, undefined, right, rightType)
} else if (right === undefined && left !== undefined) {
this.addEntry("removed", path, left, undefined, leftType)
} else if (!this.areEqual(left, right)) {
this.addEntry("changed", path, left, right, leftType)
} else if (this.options.includeUnchanged) {
this.addEntry("unchanged", path, left, right, leftType)
}
return
}
if (left === undefined && right === undefined) {
return
}
if (left === undefined) {
this.addEntry("added", path, undefined, right, rightType)
return
}
if (right === undefined) {
this.addEntry("removed", path, left, undefined, leftType)
return
}
if (this.isPrimitive(left) || this.isPrimitive(right)) {
if (!this.areEqual(left, right)) {
this.addEntry("changed", path, left, right, leftType)
} else if (this.options.includeUnchanged) {
this.addEntry("unchanged", path, left, right, leftType)
}
return
}
if (Array.isArray(left) && Array.isArray(right)) {
this.compareArrays(left, right, path, depth)
return
}
if (this.isObject(left) && this.isObject(right)) {
this.compareObjects(left, right, path, depth)
return
}
if (!this.areEqual(left, right)) {
this.addEntry("changed", path, left, right, leftType)
} else if (this.options.includeUnchanged) {
this.addEntry("unchanged", path, left, right, leftType)
}

Comment on lines +163 to +174
private shouldIgnorePath(path: string[]): boolean {
const humanPath = this.createHumanPath(path)
return this.options.ignorePaths.some((ignorePath) => {
if (ignorePath.includes("*")) {
const escapedPattern = ignorePath
.replace(/\./g, "\\.")
.replace(/\*/g, ".*")
const pattern = new RegExp("^" + escapedPattern + "$")
return pattern.test(humanPath)
}
return humanPath === ignorePath || humanPath.startsWith(ignorePath + ".")
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix wildcard ignore matching and respect custom separators.

Replacing only dots and stars leaves characters like [ and ] unescaped, so patterns such as module.rules.[*] become module\.rules\.[.*] and never match array paths. The literal "." in ignorePath + "." also breaks when callers use a custom pathSeparator. Please escape all regex metacharacters before expanding wildcards and build the prefix check with the configured separator instead:

-    return this.options.ignorePaths.some((ignorePath) => {
-      if (ignorePath.includes("*")) {
-        const escapedPattern = ignorePath
-          .replace(/\./g, "\\.")
-          .replace(/\*/g, ".*")
-        const pattern = new RegExp("^" + escapedPattern + "$")
-        return pattern.test(humanPath)
-      }
-      return humanPath === ignorePath || humanPath.startsWith(ignorePath + ".")
+    const separator = this.options.pathSeparator
+    return this.options.ignorePaths.some((ignorePath) => {
+      if (ignorePath.includes("*")) {
+        const escapedSegments = ignorePath
+          .split("*")
+          .map((segment) =>
+            segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
+          )
+        const pattern = new RegExp(
+          "^" + escapedSegments.join(".*") + "$"
+        )
+        return pattern.test(humanPath)
+      }
+      return (
+        humanPath === ignorePath ||
+        humanPath.startsWith(ignorePath + separator)
+      )
     })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private shouldIgnorePath(path: string[]): boolean {
const humanPath = this.createHumanPath(path)
return this.options.ignorePaths.some((ignorePath) => {
if (ignorePath.includes("*")) {
const escapedPattern = ignorePath
.replace(/\./g, "\\.")
.replace(/\*/g, ".*")
const pattern = new RegExp("^" + escapedPattern + "$")
return pattern.test(humanPath)
}
return humanPath === ignorePath || humanPath.startsWith(ignorePath + ".")
})
private shouldIgnorePath(path: string[]): boolean {
const humanPath = this.createHumanPath(path)
const separator = this.options.pathSeparator
return this.options.ignorePaths.some((ignorePath) => {
if (ignorePath.includes("*")) {
const escapedSegments = ignorePath
.split("*")
.map((segment) =>
segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
)
const pattern = new RegExp(
"^" + escapedSegments.join(".*") + "$"
)
return pattern.test(humanPath)
}
return (
humanPath === ignorePath ||
humanPath.startsWith(ignorePath + separator)
)
})
}

@justin808 justin808 force-pushed the config-diff-engine branch 2 times, most recently from 155f920 to 0633d0d Compare October 12, 2025 06:07
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (2)
package/configDiffer/diffEngine.ts (1)

163-175: Robust wildcard ignores: escape meta, honor custom separator, and reduce ReDoS risk

Current logic:

  • Escapes only “.” and “*”; brackets and other metacharacters break patterns like module.rules.[*].
  • Hardcodes ".", ignoring pathSeparator.
  • Regex .* can be abused for backtracking.

Prefer a safe glob or, at minimum, fully escape segments and respect pathSeparator.

   private shouldIgnorePath(path: string[]): boolean {
     const humanPath = this.createHumanPath(path)
-    return this.options.ignorePaths.some((ignorePath) => {
-      if (ignorePath.includes("*")) {
-        const escapedPattern = ignorePath
-          .replace(/\./g, "\\.")
-          .replace(/\*/g, ".*")
-        const pattern = new RegExp("^" + escapedPattern + "$")
-        return pattern.test(humanPath)
-      }
-      return humanPath === ignorePath || humanPath.startsWith(ignorePath + ".")
-    })
+    const sep = this.options.pathSeparator
+    return this.options.ignorePaths.some((ignorePath) => {
+      if (ignorePath.includes("*")) {
+        const escapedSegments = ignorePath
+          .split("*")
+          .map((segment) => segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
+        const pattern = new RegExp("^" + escapedSegments.join(".*") + "$")
+        return pattern.test(humanPath)
+      }
+      return humanPath === ignorePath || humanPath.startsWith(ignorePath + sep)
+    })
   }

Alternatively, replace the regex with a small glob scan to avoid ReDoS entirely.

package/configDiffer/formatter.ts (1)

246-250: Tighten key match for output.filename to avoid false positives.

Use exact or prefix segment match instead of substring includes. This was flagged earlier; applying here prevents unintended matches.

-    if (path.includes("output.filename")) {
+    if (path === "output.filename" || path.startsWith("output.filename.")) {
       if (String(newVal).includes("[contenthash]")) {
         return "Cache busting enabled - better long-term caching"
       }
     }
🧹 Nitpick comments (8)
bin/diff-bundler-config (1)

7-11: Differentiate “diff found” vs “CLI error” and print robust errors

Use exit code 2 for unexpected failures and surface stack when available. This makes CI logic reliable and debuggable.

 run(process.argv.slice(2))
-  .then((exitCode) => process.exit(exitCode))
-  .catch((error) => {
-    console.error(error.message)
-    process.exit(1)
-  })
+  .then((code) => process.exit(code ?? 0))
+  .catch((err) => {
+    const msg = err && (err.stack || err.message) ? (err.stack || err.message) : String(err)
+    console.error(msg)
+    process.exit(2)
+  })
docs/config-diff.md (3)

9-39: Use proper headings instead of bold to satisfy markdownlint and improve structure

Convert emphatic lines to headings.

-**Configuration-Specific Intelligence**
+### Configuration-specific intelligence
...
-**Path Normalization**
+### Path normalization
...
-**Focused Analysis**
+### Focused analysis
...
-**Multiple Output Formats**
+### Multiple output formats
...
-**Semantic Understanding**
+### Semantic understanding

329-351: Add language to fenced block (markdownlint MD040)

Specify a language for the command reference example.

-```
+```bash
 bin/diff-bundler-config --left=<file1> --right=<file2> [options]
 
 Required:
   --left=<file>              First (left) config file to compare
   --right=<file>             Second (right) config file to compare

355-357: Recommend distinct exit code for errors

Using the same code (1) for “differences” and “errors” makes CI brittle. Suggest: 0 = equal, 1 = differences, 2 = error. Update docs and CLI accordingly.

- - `1` - Differences found or error occurred
+ - `1` - Differences found
+ - `2` - Error occurred

Confirm CLI returns 2 on errors (see bin change suggestion).

test/configDiffer/pathNormalizer.test.js (1)

20-32: Add coverage for URL/module-specifier exclusions and Windows paths

Strengthen guarantees around “exclude URLs and @-scoped specifiers” and OS variance.

   describe("normalize", () => {
+    test("does not normalize URLs and module specifiers", () => {
+      const normalizer = new PathNormalizer("/app")
+      const config = {
+        devtool: "webpack://",
+        source: "http://example.com/file.js",
+        pkg: "@scope/package"
+      }
+      const result = normalizer.normalize(config)
+      expect(result.normalized.devtool).toBe("webpack://")
+      expect(result.normalized.source).toBe("http://example.com/file.js")
+      expect(result.normalized.pkg).toBe("@scope/package")
+    })
+
+    test("normalizes Windows drive paths to relative", () => {
+      const basePath = "C:\\app\\project"
+      const normalizer = new PathNormalizer(basePath)
+      const config = { out: "C:\\app\\project\\public\\packs" }
+      const result = normalizer.normalize(config)
+      expect(result.normalized.out).toBe("./public/packs")
+    })

Also applies to: 67-77, 79-89

test/configDiffer/diffEngine.test.js (2)

120-140: Add tests for custom pathSeparator and bracket wildcards in ignorePaths

Ensure ignores work when using non-dot separators and when matching array indices.

   describe("options", () => {
+    test("respects custom pathSeparator in ignorePaths prefix check", () => {
+      const engine = new DiffEngine({
+        pathSeparator: "/",
+        ignorePaths: ["plugins/*"]
+      })
+      const result = engine.compare(
+        { plugins: { a: 1 }, other: 1 },
+        { plugins: { a: 2 }, other: 2 }
+      )
+      expect(result.summary.totalChanges).toBe(1)
+      expect(result.entries[0].path.humanPath).toBe("other")
+    })
+
+    test("supports bracket wildcard for array indices", () => {
+      const engine = new DiffEngine({ ignorePaths: ["rules.[*]"] })
+      const result = engine.compare(
+        { rules: [{ test: "a" }, { test: "b" }], x: 1 },
+        { rules: [{ test: "A" }, { test: "B" }], x: 2 }
+      )
+      expect(result.summary.totalChanges).toBe(1)
+      expect(result.entries[0].path.humanPath).toBe("x")
+    })

Also applies to: 142-151


153-171: Add test to catch valueType bug when newValue is falsy but type changes

Prevents regressions around valueType inference.

   describe("special types", () => {
+    test("valueType reflects newValue even when falsy", () => {
+      const engine = new DiffEngine()
+      const result = engine.compare({ a: "1" }, { a: 0 })
+      expect(result.entries[0].operation).toBe("changed")
+      expect(result.entries[0].valueType).toBe("number")
+    })
test/configDiffer/formatter.test.js (1)

91-129: Add direct tests for formatContextual (default CLI format).

We only test formatDetailed (alias) but not formatContextual explicitly. Add coverage to lock headers, legend, labels, and entry rendering.

@@
   describe("formatDetailed", () => {
@@
   })
+
+  describe("formatContextual", () => {
+    test("renders header, comparing lines, legend, and entries", () => {
+      const formatter = new DiffFormatter()
+      const output = formatter.formatContextual(mockResult)
+
+      expect(output).toContain("Webpack/Rspack Configuration Comparison")
+      expect(output).toContain("Comparing:")
+      expect(output).toContain("Legend:")
+      expect(output).toContain("[+]")
+      expect(output).toContain("[-]")
+      expect(output).toContain("[~]")
+      expect(output).toContain("newKey")
+      expect(output).toContain("oldKey")
+      expect(output).toContain("changedKey")
+      expect(output).toMatch(/Values:\s+.+:\s/)
+    })
+  })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0633d0d and a2e4bdd.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (17)
  • README.md (1 hunks)
  • bin/diff-bundler-config (1 hunks)
  • docs/config-diff.md (1 hunks)
  • lib/install/bin/diff-bundler-config (1 hunks)
  • package.json (1 hunks)
  • package/configDiffer/cli.ts (1 hunks)
  • package/configDiffer/configDocs.ts (1 hunks)
  • package/configDiffer/diffEngine.ts (1 hunks)
  • package/configDiffer/formatter.ts (1 hunks)
  • package/configDiffer/index.ts (1 hunks)
  • package/configDiffer/pathNormalizer.ts (1 hunks)
  • package/configDiffer/types.ts (1 hunks)
  • test/configDiffer/diffEngine.test.js (1 hunks)
  • test/configDiffer/formatter.test.js (1 hunks)
  • test/configDiffer/pathNormalizer.test.js (1 hunks)
  • tmp/examples/dev-config.yaml (1 hunks)
  • tmp/examples/prod-config.yaml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (9)
  • package/configDiffer/configDocs.ts
  • package.json
  • package/configDiffer/index.ts
  • lib/install/bin/diff-bundler-config
  • package/configDiffer/types.ts
  • tmp/examples/prod-config.yaml
  • tmp/examples/dev-config.yaml
  • package/configDiffer/cli.ts
  • README.md
🧰 Additional context used
🧬 Code graph analysis (6)
test/configDiffer/diffEngine.test.js (1)
package/configDiffer/diffEngine.ts (1)
  • DiffEngine (9-299)
package/configDiffer/pathNormalizer.ts (2)
package/configDiffer/index.ts (2)
  • PathNormalizer (4-4)
  • NormalizedConfig (11-11)
package/configDiffer/types.ts (1)
  • NormalizedConfig (44-48)
package/configDiffer/diffEngine.ts (1)
package/configDiffer/types.ts (5)
  • DiffOptions (34-42)
  • DiffEntry (8-14)
  • DiffResult (16-32)
  • DiffOperation (1-1)
  • DiffPath (3-6)
package/configDiffer/formatter.ts (2)
package/configDiffer/types.ts (3)
  • DiffResult (16-32)
  • DiffEntry (8-14)
  • DiffOperation (1-1)
package/configDiffer/configDocs.ts (1)
  • getDocForKey (161-163)
test/configDiffer/pathNormalizer.test.js (1)
package/configDiffer/pathNormalizer.ts (1)
  • PathNormalizer (4-167)
test/configDiffer/formatter.test.js (1)
package/configDiffer/formatter.ts (1)
  • DiffFormatter (5-289)
🪛 markdownlint-cli2 (0.18.1)
docs/config-diff.md

9-9: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


15-15: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


21-21: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


27-27: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


34-34: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


329-329: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (2)
docs/config-diff.md (1)

216-226: Default output format: contextual vs detailed — please confirm

Docs say “Detailed (default)”; PR summary mentions a new contextual default. DiffEngine currently defaults to "detailed". Align CLI behavior and docs.

Would you like the CLI default to be contextual? If yes, update both CLI help/docs and the DiffFormatter/CLI default accordingly. Based on learnings.

package/configDiffer/diffEngine.ts (1)

51-53: Max depth semantics: do you want an aggregated “elided” entry instead of hiding diffs?

Early return makes deep differences invisible (summary=0). Consider recording an aggregated added/removed/changed (or unchanged when requested) at the current path when depth is exceeded.

If desired, I can provide a scoped patch that adds a single aggregated entry at path when depth > maxDepth without descending.

Comment on lines +140 to +143
oldValue: this.serializeValue(oldValue),
newValue: this.serializeValue(newValue),
valueType: valueType || this.getValueType(newValue || oldValue)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix valueType when newValue is falsy (e.g., 0, "", false)

newValue || oldValue misreports type. Use an undefined check.

-      valueType: valueType || this.getValueType(newValue || oldValue)
+      valueType: valueType ?? this.getValueType(newValue !== undefined ? newValue : oldValue)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
oldValue: this.serializeValue(oldValue),
newValue: this.serializeValue(newValue),
valueType: valueType || this.getValueType(newValue || oldValue)
}
oldValue: this.serializeValue(oldValue),
newValue: this.serializeValue(newValue),
valueType: valueType ?? this.getValueType(newValue !== undefined ? newValue : oldValue)
}
🤖 Prompt for AI Agents
In package/configDiffer/diffEngine.ts around lines 140 to 143, the expression
`newValue || oldValue` incorrectly treats valid falsy values (0, "", false) as
absent when determining valueType; change the selection to use an undefined
check (e.g., use `newValue === undefined ? oldValue : newValue`) so that only
undefined falls back to oldValue, and keep using the provided valueType when
present.

Comment on lines +97 to +99
const basename = filename.split("/").pop() || filename
const withoutExt = basename.replace(/\.(yaml|yml|json|js|ts)$/, "")

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Cross‑platform filename parsing: support backslashes.

split("/") fails on Windows paths. Use a cross‑platform pattern.

-    const basename = filename.split("/").pop() || filename
+    const basename = filename.split(/[\\/]/).pop() || filename
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const basename = filename.split("/").pop() || filename
const withoutExt = basename.replace(/\.(yaml|yml|json|js|ts)$/, "")
const basename = filename.split(/[\\/]/).pop() || filename
const withoutExt = basename.replace(/\.(yaml|yml|json|js|ts)$/, "")
🤖 Prompt for AI Agents
In package/configDiffer/formatter.ts around lines 97 to 99, filename parsing
uses split("/") which breaks on Windows paths; replace it with Node's
cross-platform path handling: import the built-in path module (e.g., const path
= require('path') or an ES import), call path.basename(filename) to obtain the
basename, then run the existing extension-stripping regex on that basename;
update imports accordingly and remove the split("/") usage.

Comment on lines +41 to +55
private normalizePath(str: string): string {
if (!this.looksLikePath(str)) {
return str
}

const absolutePath = isAbsolute(str) ? str : resolve(this.basePath, str)

const relativePath = relative(this.basePath, absolutePath)

if (relativePath && !relativePath.startsWith("..")) {
return "./" + relativePath.split(sep).join("/")
}

return str
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Cross‑platform bug: Windows paths can normalize incorrectly on POSIX.

normalizePath uses platform isAbsolute/resolve/relative, so a Windows path like C:\app\dist on Linux is treated as relative and becomes ./C:\app\dist. Guard by detecting path “flavor” and using path.win32/path.posix accordingly; only normalize when the basePath flavor matches.

-  private normalizePath(str: string): string {
+  private normalizePath(str: string): string {
     if (!this.looksLikePath(str)) {
       return str
     }
 
-    const absolutePath = isAbsolute(str) ? str : resolve(this.basePath, str)
-
-    const relativePath = relative(this.basePath, absolutePath)
-
-    if (relativePath && !relativePath.startsWith("..")) {
-      return "./" + relativePath.split(sep).join("/")
-    }
-
-    return str
+    const isWin = this.isWindowsLike(str)
+    const baseIsWin = this.isWindowsLike(this.basePath)
+    // Don't attempt cross-flavor normalization
+    if (isWin !== baseIsWin) {
+      return str
+    }
+    const p = isWin ? require("path").win32 : require("path").posix
+    const absolutePath = p.isAbsolute(str) ? str : p.resolve(this.basePath, str)
+    const relativePath = p.relative(this.basePath, absolutePath)
+    if (relativePath && !relativePath.startsWith("..")) {
+      return "./" + relativePath.replace(/[\\]+/g, "/")
+    }
+    return str
   }
+
+  private isWindowsLike(s: string): boolean {
+    return /^[A-Za-z]:[\\/]/.test(s) || s.startsWith("\\\\")
+  }

Comment on lines +121 to +142
static detectBasePath(config: any): string | undefined {
const paths: string[] = []

const extractPaths = (value: any): void => {
if (typeof value === "string" && isAbsolute(value)) {
paths.push(value)
} else if (Array.isArray(value)) {
value.forEach(extractPaths)
} else if (value && typeof value === "object") {
Object.values(value).forEach(extractPaths)
}
}

extractPaths(config)

if (paths.length === 0) {
return undefined
}

const commonPrefix = this.findCommonPrefix(paths)
return commonPrefix || undefined
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

detectBasePath misses Windows absolute paths on POSIX.

Use path.win32.isAbsolute in addition to the host platform check so Windows paths are recognized during detection.

-    const extractPaths = (value: any): void => {
-      if (typeof value === "string" && isAbsolute(value)) {
+    const { posix, win32 } = require("path")
+    const extractPaths = (value: any): void => {
+      if (
+        typeof value === "string" &&
+        (posix.isAbsolute(value) || win32.isAbsolute(value))
+      ) {
         paths.push(value)
       } else if (Array.isArray(value)) {
         value.forEach(extractPaths)
       } else if (value && typeof value === "object") {
         Object.values(value).forEach(extractPaths)
       }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static detectBasePath(config: any): string | undefined {
const paths: string[] = []
const extractPaths = (value: any): void => {
if (typeof value === "string" && isAbsolute(value)) {
paths.push(value)
} else if (Array.isArray(value)) {
value.forEach(extractPaths)
} else if (value && typeof value === "object") {
Object.values(value).forEach(extractPaths)
}
}
extractPaths(config)
if (paths.length === 0) {
return undefined
}
const commonPrefix = this.findCommonPrefix(paths)
return commonPrefix || undefined
}
static detectBasePath(config: any): string | undefined {
const paths: string[] = []
const { posix, win32 } = require("path")
const extractPaths = (value: any): void => {
if (
typeof value === "string" &&
(posix.isAbsolute(value) || win32.isAbsolute(value))
) {
paths.push(value)
} else if (Array.isArray(value)) {
value.forEach(extractPaths)
} else if (value && typeof value === "object") {
Object.values(value).forEach(extractPaths)
}
}
extractPaths(config)
if (paths.length === 0) {
return undefined
}
const commonPrefix = this.findCommonPrefix(paths)
return commonPrefix || undefined
}
🤖 Prompt for AI Agents
In package/configDiffer/pathNormalizer.ts around lines 121 to 142,
detectBasePath currently only uses the host-platform isAbsolute check and
therefore misses Windows-style absolute paths when running on POSIX; update the
path detection to check both platform-specific and Windows absolute paths (e.g.,
use path.win32.isAbsolute(value) in addition to the existing isAbsolute(value)),
applying this combined check inside extractPaths for string values so Windows
paths (drive letters and backslash forms) are collected and included when
computing the common prefix.

Comment on lines +153 to +166
const splitPaths = paths.map((p) => p.split(sep))
let prefix: string[] = []

for (let i = 0; i < splitPaths[0].length; i++) {
const segment = splitPaths[0][i]
if (splitPaths.every((p) => p[i] === segment)) {
prefix.push(segment)
} else {
break
}
}

return prefix.join(sep)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Common-prefix computation is platform-bound; fails with backslashes.

Split on both separators and rejoin with the appropriate one based on inputs.

-    const splitPaths = paths.map((p) => p.split(sep))
+    const splitPaths = paths.map((p) => p.split(/[\\/]+/))
     let prefix: string[] = []
@@
-    return prefix.join(sep)
+    const useWinSep = paths.every((p) => /^[A-Za-z]:[\\/]/.test(p) || p.startsWith("\\\\"))
+    return prefix.join(useWinSep ? "\\" : "/")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const splitPaths = paths.map((p) => p.split(sep))
let prefix: string[] = []
for (let i = 0; i < splitPaths[0].length; i++) {
const segment = splitPaths[0][i]
if (splitPaths.every((p) => p[i] === segment)) {
prefix.push(segment)
} else {
break
}
}
return prefix.join(sep)
}
const splitPaths = paths.map((p) => p.split(/[\\/]+/))
let prefix: string[] = []
for (let i = 0; i < splitPaths[0].length; i++) {
const segment = splitPaths[0][i]
if (splitPaths.every((p) => p[i] === segment)) {
prefix.push(segment)
} else {
break
}
}
const useWinSep = paths.every((p) => /^[A-Za-z]:[\\/]/.test(p) || p.startsWith("\\\\"))
return prefix.join(useWinSep ? "\\" : "/")
🤖 Prompt for AI Agents
In package/configDiffer/pathNormalizer.ts around lines 153 to 166, the
common-prefix logic only splits on path.sep and thus fails when inputs use
backslashes; instead normalize by splitting each input on both separators (e.g.
split on /|\\), determine which separator to use for the output by inspecting
the original inputs (if any path contains '\' use backslash, otherwise use '/'),
compute the common path segments across the normalized segment arrays, and
return those segments joined with the chosen separator; also handle empty inputs
or single-item arrays gracefully.

justin808 and others added 4 commits October 30, 2025 11:54
Implements Phase 1 of issue #667: Core Diff Engine

Features:
- Deep object diff algorithm with path-level granularity
- Support for nested objects, arrays, and special types (functions, RegExp, Date)
- Path normalization utilities for comparing absolute paths
- Multiple output formats: detailed, summary, JSON, YAML
- Advanced filtering options: ignore keys, ignore paths (with wildcards)
- Comprehensive test coverage with 39 passing tests

New CLI command:
- bin/diff-bundler-config: Compare two config files
- Supports JSON, YAML, JS, and TS config files
- Exit code 0 if no differences, 1 if differences found

Documentation:
- Complete diff tool guide in docs/config-diff.md
- Updated README.md with diff examples
- Usage examples and tips

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Add configDiffer to package.json exports for proper module resolution
- Add comprehensive 'Why use this?' section explaining advantages over visual diff
- Document 5 common troubleshooting workflows with examples
- Explain integration with --doctor mode
- Add practical guidance on what to look for in different scenarios

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…ation

Address code review feedback:

1. Fix basePath detection to use separate paths for left/right configs
   - Each side now detects its own base path independently
   - More accurate normalization when comparing configs from different locations

2. Remove no-op map in createHumanPath
   - Simplified path formatting by removing pointless transformation
   - Cleaner, more maintainable code

3. Fix looksLikePath to avoid normalizing URLs and module specifiers
   - Explicitly exclude URL schemes (http://, webpack://, etc.)
   - Exclude npm module specifiers starting with @
   - Only match actual filesystem paths (absolute, relative, home, Windows drives)
   - Prevents incorrect normalization of webpack:// URLs and @scope/package names

4. Replace summary/detailed formats with contextual format
   - Shows "What it does" explanations for webpack config keys
   - Includes impact analysis (e.g., "Enabling production optimizations")
   - Links to official webpack documentation
   - Displays default values where applicable
   - Much more useful for understanding WHY something changed

5. Add comprehensive configuration documentation database
   - 20+ documented webpack/rspack configuration keys
   - Covers mode, devtool, optimization, output, devServer, etc.
   - Extensible for future additions

New output format example:

    1. [~] mode

       What it does:
       Defines the environment mode (development, production, or none).
       Controls built-in optimizations and defaults.

       Affects: Minification, tree-shaking, source maps, and performance optimizations

       Old value: "development"
       New value: "production"

       Impact: Enabling production optimizations (minification, tree-shaking)

       Documentation: https://webpack.js.org/configuration/mode/

Summary format is now concise: "8 changes: +3 -1 ~4"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Changes based on feedback:

1. Remove redundant value type display
   - Type information doesn't add value to the diff output
   - Users can see the type from the value itself

2. Replace "old/new" with file-based labels
   - Extract meaningful short names from filenames
   - webpack-development-client.yaml -> "dev-client"
   - webpack-production-server.yaml -> "prod-server"
   - Falls back to "left"/"right" if no pattern matches

3. Show values in a clearer format:
   Before:
     Old value: "development"
     New value: "production"

   After:
     Values:
       dev-client:  "development"
       prod-client: "production"

4. Design supports future N-way comparison
   - Format naturally extends to comparing 3+ configs
   - Could show dev-client, dev-server, prod-client, prod-server all in one view
   - Labels make it clear which value comes from which file

Example output:

    3. [~] mode

       What it does:
       Defines the environment mode (development, production, or none).

       Affects: Minification, tree-shaking, source maps

       Values:
         dev:  "development"
         prod: "production"

       Impact: Enabling production optimizations

Much clearer which config has which value!

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (9)
package/configDiffer/diffEngine.ts (3)

130-143: Don’t treat falsy new values as “missing”.

The newValue || oldValue fallback on Line 142 misreports the type for legitimate falsy values (e.g., 0 → “number” becomes “string” if oldValue was "0"). Use an undefined check instead.

-      valueType: valueType || this.getValueType(newValue || oldValue)
+      valueType:
+        valueType ??
+        this.getValueType(newValue !== undefined ? newValue : oldValue)

163-175: Wildcard ignore logic breaks on array paths and custom separators.

The current expansion (Line 166) leaves [ ] and other metacharacters unescaped, so a pattern like module.rules[*] never matches array entries. Additionally, the prefix check hardcodes ".", ignoring --path-separator. Escape all regex metacharacters and honor the configured separator.

-    const humanPath = this.createHumanPath(path)
-    return this.options.ignorePaths.some((ignorePath) => {
-      if (ignorePath.includes("*")) {
-        const escapedPattern = ignorePath
-          .replace(/\./g, "\\.")
-          .replace(/\*/g, ".*")
-        const pattern = new RegExp("^" + escapedPattern + "$")
-        return pattern.test(humanPath)
-      }
-      return humanPath === ignorePath || humanPath.startsWith(ignorePath + ".")
-    })
+    const humanPath = this.createHumanPath(path)
+    const separator = this.options.pathSeparator
+    return this.options.ignorePaths.some((ignorePath) => {
+      if (ignorePath.includes("*")) {
+        const escapedSegments = ignorePath
+          .split("*")
+          .map((segment) => segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
+        const pattern = new RegExp("^" + escapedSegments.join(".*") + "$")
+        return pattern.test(humanPath)
+      }
+      return (
+        humanPath === ignorePath ||
+        humanPath.startsWith(ignorePath + separator)
+      )
+    })

51-78: Respect maxDepth without hiding real differences.

The guard on Line 51 returns immediately, so with --max-depth=0 every nested change disappears and the diff falsely reports “no differences”. Emit an aggregated entry before returning.

-    if (this.options.maxDepth !== null && depth > this.options.maxDepth) {
-      return
-    }
-
-    const leftType = this.getValueType(left)
-    const rightType = this.getValueType(right)
+    const leftType = this.getValueType(left)
+    const rightType = this.getValueType(right)
+
+    if (this.options.maxDepth !== null && depth > this.options.maxDepth) {
+      if (left === undefined && right !== undefined) {
+        this.addEntry("added", path, undefined, right, rightType)
+      } else if (right === undefined && left !== undefined) {
+        this.addEntry("removed", path, left, undefined, leftType)
+      } else if (!this.areEqual(left, right)) {
+        this.addEntry("changed", path, left, right, leftType)
+      } else if (this.options.includeUnchanged) {
+        this.addEntry("unchanged", path, left, right, leftType)
+      }
+      return
+    }
package/configDiffer/formatter.ts (1)

246-249: Match output.filename precisely (respecting custom separators).

Line 246 matches any path containing the substring, so a key like module.rules[0].output.filenameTemplate triggers the cache-busting note, and using a non-dot separator stops matching altogether. Compare against exact segment names.

-    if (path.includes("output.filename")) {
+    if (
+      path === "output.filename" ||
+      path.startsWith("output.filename" + ".")
+    ) {
       if (String(newVal).includes("[contenthash]")) {
         return "Cache busting enabled - better long-term caching"
       }
     }

If you add custom pathSeparator support here too, reuse the same separator the formatter already exposes.

package/configDiffer/pathNormalizer.ts (3)

124-141: Detect absolute paths from both POSIX and Windows inputs.

Line 125 only checks path.isAbsolute, so C:\project\dist never seeds detectBasePath on POSIX. Use both posixPath.isAbsolute and win32Path.isAbsolute so cross-platform exports share the same base path.

-      if (typeof value === "string" && isAbsolute(value)) {
+      if (
+        typeof value === "string" &&
+        (posixPath.isAbsolute(value) || win32Path.isAbsolute(value))
+      ) {
         paths.push(value)

149-166: Handle mixed separators when computing the common prefix.

split(sep) fails for Windows-style strings while running on POSIX (Line 153), so the common prefix collapses to the whole path and normalization regresses. Split on both separators and choose the appropriate joiner.

-    const splitPaths = paths.map((p) => p.split(sep))
+    const splitPaths = paths.map((p) => p.split(/[\\/]+/))
@@
-    return prefix.join(sep)
+    const useWinSeparator = paths.some((p) => p.includes("\\"))
+    return prefix.join(useWinSeparator ? "\\" : "/")

41-55: Stop mangling Windows paths when normalizing on POSIX.

On Line 46 we feed any string with backslashes into the host path.resolve/relative. Running this on macOS/Linux with C:\build\dist rewrites the value to something like ./C:\build\dist, so the diff output lies about the original path. Detect the path “flavor”, bail on cross-flavor cases, and use path.win32/path.posix helpers accordingly.

-import { resolve, isAbsolute, relative, sep } from "path"
+import { sep, posix as posixPath, win32 as win32Path } from "path"
@@
   private normalizePath(str: string): string {
     if (!this.looksLikePath(str)) {
       return str
     }
 
-    const absolutePath = isAbsolute(str) ? str : resolve(this.basePath, str)
-
-    const relativePath = relative(this.basePath, absolutePath)
+    const pathModule = this.isWindowsLike(str) ? win32Path : posixPath
+    const baseIsWindows = this.isWindowsLike(this.basePath)
+    if (baseIsWindows !== this.isWindowsLike(str)) {
+      return str
+    }
+
+    const absolutePath = pathModule.isAbsolute(str)
+      ? str
+      : pathModule.resolve(this.basePath, str)
+    const relativePath = pathModule.relative(this.basePath, absolutePath)
 
     if (relativePath && !relativePath.startsWith("..")) {
-      return "./" + relativePath.split(sep).join("/")
+      return "./" + relativePath.replace(/[\\]+/g, "/")
     }
 
     return str
   }
+
+  private isWindowsLike(value: string): boolean {
+    return /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("\\\\")
+  }
package/configDiffer/cli.ts (2)

76-82: Dynamic require('fs') is redundant.

Line 77 uses require("fs") despite fs being imported via named imports at the top of the file (line 1: import { existsSync, readFileSync } from "fs"). This creates inconsistency and was flagged in a previous review but remains unaddressed.

Import writeFileSync at the top and use it directly:

-import { existsSync, readFileSync } from "fs"
+import { existsSync, readFileSync, writeFileSync } from "fs"
 import { resolve, extname } from "path"
 import { load as loadYaml } from "js-yaml"

Then update the file writing code:

     if (options.output) {
-      const fs = require("fs")
-      fs.writeFileSync(options.output, output, "utf8")
+      writeFileSync(options.output, output, "utf8")
       console.log(`Diff written to: ${options.output}`)
     } else {

183-190: Require cache clearing limitation remains undocumented.

Line 183 clears the require cache, but this only works for CommonJS modules. For ES modules, configs won't reload properly in the same process. This was flagged in a previous review but the limitation is still not documented.

Add a comment documenting this limitation:

     delete require.cache[resolvedPath]
+    // Note: Cache clearing only works for CommonJS modules.
+    // For ES modules, configs may not reload properly in the same process.
     let loaded = require(resolvedPath)

     if (typeof loaded === "object" && "default" in loaded) {
       loaded = loaded.default
     }
🧹 Nitpick comments (4)
docs/config-diff.md (2)

9-38: Promote emphasis blocks to real headings.

Markdownlint (MD036) flags the repeated **Heading** pattern starting around Line 9. Turning these into level-3 headings (e.g., ### Configuration-Specific Intelligence) keeps the styling and satisfies the linter.


329-351: Add a language tag to the command reference fence.

Markdownlint (MD040) complains because the block beginning near Line 330 lacks a language. Add text or bash after the opening ``` to clear the warning.

test/configDiffer/formatter.test.js (1)

4-37: Consider removing unused valueType field from mock data.

The valueType field appears on lines 16, 22, and 29 but doesn't seem to be used by the DiffFormatter based on the provided formatter.ts code. This might be legacy data or an implementation detail that's no longer surfaced in the output.

If valueType is truly unused, apply this diff:

       {
         operation: "added",
         path: { path: ["newKey"], humanPath: "newKey" },
-        newValue: "newValue",
-        valueType: "string"
+        newValue: "newValue"
       },
       {
         operation: "removed",
         path: { path: ["oldKey"], humanPath: "oldKey" },
-        oldValue: "oldValue",
-        valueType: "string"
+        oldValue: "oldValue"
       },
       {
         operation: "changed",
         path: { path: ["changedKey"], humanPath: "changedKey" },
         oldValue: "oldValue",
-        newValue: "newValue",
-        valueType: "string"
+        newValue: "newValue"
       }
package/configDiffer/cli.ts (1)

114-151: Unknown CLI arguments are silently ignored.

The argument parser doesn't validate or warn about unrecognized arguments. If a user mistypes --formt=json instead of --format=json, the tool will silently use the default format, which could cause confusion.

Consider tracking and warning about unknown arguments:

   const parseValue = (arg: string, prefix: string): string => {
     const value = arg.substring(prefix.length)
     if (value.length === 0) {
       throw new Error(`${prefix} requires a value`)
     }
     return value
   }

+  const unknownArgs: string[] = []
+
   for (const arg of args) {
     if (arg === "--help" || arg === "-h") {
       options.help = true
     } else if (arg.startsWith("--left=")) {
       // ... existing cases ...
     } else if (arg.startsWith("--path-separator=")) {
       options.pathSeparator = parseValue(arg, "--path-separator=")
+    } else if (arg.startsWith("--")) {
+      unknownArgs.push(arg)
     }
   }

+  if (unknownArgs.length > 0 && !options.help) {
+    console.warn(`Warning: Unknown arguments ignored: ${unknownArgs.join(", ")}`)
+  }
+
   return options
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2e4bdd and 50c1935.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (17)
  • README.md (1 hunks)
  • bin/diff-bundler-config (1 hunks)
  • docs/config-diff.md (1 hunks)
  • lib/install/bin/diff-bundler-config (1 hunks)
  • package.json (1 hunks)
  • package/configDiffer/cli.ts (1 hunks)
  • package/configDiffer/configDocs.ts (1 hunks)
  • package/configDiffer/diffEngine.ts (1 hunks)
  • package/configDiffer/formatter.ts (1 hunks)
  • package/configDiffer/index.ts (1 hunks)
  • package/configDiffer/pathNormalizer.ts (1 hunks)
  • package/configDiffer/types.ts (1 hunks)
  • test/configDiffer/diffEngine.test.js (1 hunks)
  • test/configDiffer/formatter.test.js (1 hunks)
  • test/configDiffer/pathNormalizer.test.js (1 hunks)
  • tmp/examples/dev-config.yaml (1 hunks)
  • tmp/examples/prod-config.yaml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (9)
  • lib/install/bin/diff-bundler-config
  • test/configDiffer/pathNormalizer.test.js
  • package/configDiffer/configDocs.ts
  • package/configDiffer/types.ts
  • package.json
  • test/configDiffer/diffEngine.test.js
  • package/configDiffer/index.ts
  • tmp/examples/prod-config.yaml
  • README.md
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: shakacode/shakapacker PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-19T00:36:13.668Z
Learning: Applies to lib/shakapacker/**/*.rb : When modifying core Shakapacker functionality, test changes with both webpack and rspack bundlers
📚 Learning: 2025-10-19T00:36:13.668Z
Learnt from: CR
Repo: shakacode/shakapacker PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-19T00:36:13.668Z
Learning: Applies to lib/shakapacker/**/*.rb : When modifying core Shakapacker functionality, test changes with both webpack and rspack bundlers

Applied to files:

  • tmp/examples/dev-config.yaml
  • bin/diff-bundler-config
  • docs/config-diff.md
🧬 Code graph analysis (5)
test/configDiffer/formatter.test.js (2)
package/configDiffer/formatter.ts (1)
  • DiffFormatter (5-289)
package/configDiffer/index.ts (1)
  • DiffFormatter (3-3)
package/configDiffer/pathNormalizer.ts (2)
package/configDiffer/index.ts (2)
  • PathNormalizer (4-4)
  • NormalizedConfig (11-11)
package/configDiffer/types.ts (1)
  • NormalizedConfig (44-48)
package/configDiffer/cli.ts (4)
package/configDiffer/types.ts (1)
  • DiffOptions (34-42)
package/configDiffer/pathNormalizer.ts (1)
  • PathNormalizer (4-167)
package/configDiffer/diffEngine.ts (1)
  • DiffEngine (9-299)
package/configDiffer/formatter.ts (1)
  • DiffFormatter (5-289)
package/configDiffer/diffEngine.ts (1)
package/configDiffer/types.ts (5)
  • DiffOptions (34-42)
  • DiffEntry (8-14)
  • DiffResult (16-32)
  • DiffOperation (1-1)
  • DiffPath (3-6)
package/configDiffer/formatter.ts (2)
package/configDiffer/types.ts (3)
  • DiffResult (16-32)
  • DiffEntry (8-14)
  • DiffOperation (1-1)
package/configDiffer/configDocs.ts (1)
  • getDocForKey (161-163)
🪛 markdownlint-cli2 (0.18.1)
docs/config-diff.md

9-9: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


15-15: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


21-21: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


27-27: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


34-34: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


329-329: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: claude-review
🔇 Additional comments (6)
tmp/examples/dev-config.yaml (2)

14-15: Clarify if simplified plugin representation is intentional.

The plugins array (lines 14–15) lists plugins as strings rather than instantiated objects, which deviates from standard webpack configuration syntax. While this may be intentional for a simplified example file, please confirm:

  1. Is this meant to be a schematic/documentation example rather than an executable config?
  2. Should plugins be shown as fully instantiated objects (e.g., { name: HtmlWebpackPlugin, options: {...} }) for more realistic representation?

This matters for validating that users can rely on this as a reference when comparing real configurations.


1-18: Example file looks good overall.

The configuration provides a clear, minimal example of webpack development settings (mode, devtool, output, cache, optimization, plugins, devServer) and is well-suited for demonstration and testing of the diff engine. The YAML syntax is valid.

If this is intended as a reference template for users, consider either: (1) making a note in accompanying docs that it's a simplified schema, or (2) expanding plugins to show realistic object structure. For internal testing/fixtures, the current form is acceptable.

bin/diff-bundler-config (1)

1-11: LGTM.

The shim cleanly delegates to the shared CLI handler and exits with the provided code.

test/configDiffer/formatter.test.js (1)

39-129: Tests provide adequate basic coverage.

The test suite validates the four main formatter outputs (JSON, YAML, summary, detailed) with appropriate assertions for structure and content presence. The tests are well-organized and sufficient for smoke-testing the formatter's output formats.

package/configDiffer/cli.ts (2)

38-49: Per-side base path detection correctly implemented.

The code now detects base paths independently for left and right configs (lines 39-42) and creates separate normalizers (lines 44-45), addressing the previous review concern about cross-machine path normalization.


16-89: CLI structure and implementation are well-designed.

The overall CLI implementation is clean and well-organized:

  • Clear separation of concerns (run, parseArguments, loadConfigFile, showHelp)
  • Comprehensive help text with examples
  • Proper error handling and exit codes
  • Support for multiple input formats (JSON, YAML, JS, TS)
  • Good integration with DiffEngine, DiffFormatter, and PathNormalizer

Also applies to: 91-196, 198-260

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants