Packwerk Documentation

repository·main·Indexed 23 days ago

https://github.com/shopify/packwerk

Packwerk is a Ruby gem used to enforce boundaries and modularize Rails applications by grouping files into packages and controlling constant visibility. It performs static analysis on constant references to identify dependency violations and help existing codebases become more modular. It requires Ruby MRI 2.7+ and Zeitwerk. Key features include a CLI for running checks, initializing configurations, and updating todo files, as well as a flexible configuration system via packwerk.yml.

Tokens
8.4K
Snippets
21
Records
48
Agent score
83%

What's inside Packwerk

  1. What is Packwerk and how does it work?

    main

    Packwerk is a Ruby gem designed to enforce boundaries and modularize Rails applications. It allows you to:

    • Group files into packages: Define logical boundaries within your codebase.
    • Define package-level constant visibility: Control which constants are publicly accessible from outside a package.
    • Incremental modularization: Help existing codebases become more modular without obstructing active development.

    Core Mental Model: Packwerk performs static analysis on constant references. It does not analyze method calls or objects passed around the application. If you need to analyze method parameters, you should use Sorbet type signatures, as Packwerk understands Sorbet's use of constants for types.

  2. Define a package with package.yml

    main

    A package is defined by placing a package.yml file in a folder. The package name is the path from the project root to that folder.

    You can include arbitrary metadata in package.yml (e.g., for ownership or Slack channels).

    Example components/sales/package.yml:

    metadata:
      stewards:
      - "@Shopify/sales"
      slack_channels:
      - "#sales"
    # components/sales/package.yml
    metadata:
      stewards:
      - "@Shopify/sales"
      slack_channels:
      - "#sales"
  3. Use strict mode for dependency enforcement

    main

    To prevent new violations from being added to a package's package_todo.yml, use strict mode.

    In the package's package.yml, set enforce_dependencies: strict instead of true.

    When strict mode is enabled:

    1. Running bin/packwerk check will error if any new violations are found.
    2. Running bin/packwerk update-todo will return an error instead of adding new violations to the todo file.
    enforce_dependencies: strict
  4. Understand Packwerk limitations

    main

    Because Ruby is a dynamic language, Packwerk prioritizes avoiding false positives (reporting valid code as a violation) over avoiding false negatives (failing to report an actual violation).

    Key Limitations

    • Constant Resolution: Packwerk can only resolve references to constants defined via Zeitwerk's autoloader. Code loaded via explicit require that bypasses Zeitwerk conventions may cause issues.
    • Scope of Analysis: Packwerk only cares about static constant references. It ignores method calls and object passing. To analyze method parameters, use Sorbet.
    • Zeitwerk Configuration: While Packwerk supports custom ActiveSupport inflections, it may produce false positives if you use custom Zeitwerk inflections or directory collapsing directly in your Zeitwerk configuration.
  5. Enforce dependency boundaries

    main

    To prevent a package from referencing constants in other packages without explicit permission, enable dependency enforcement in the package's package.yml.

    Set enforce_dependencies: true and list the allowed packages under dependencies:.

    Example components/shop_identity/package.yml:

    enforce_dependencies: true
    dependencies:
      - components/platform

    In this example, components/shop_identity is prohibited from referencing any constant outside of itself or components/platform.

    # components/shop_identity/package.yml
    enforce_dependencies: true
    dependencies:
      - components/platform
  6. Install Packwerk

    main

    To install Packwerk in your Rails application, add the gem to your Gemfile, install it, generate a binstub, and initialize the configuration files.

    Prerequisites

    • Zeitwerk: Must be enabled (standard in Rails 6+).
    • Ruby: MRI version 2.7 or above.
  7. Speed up Packwerk feedback locally

    main

    To avoid waiting for long Continuous Integration (CI) cycles, run Packwerk commands locally on the command line. You can significantly reduce run time by specifying only the specific folders or packages you are working on.

    Note: You cannot limit the scope for bin/packwerk validate or bin/packwerk update-todo; these commands always run against the entire application.

    bin/packwerk check components/your_package
  8. Install and initialize Packwerk

    main

    To get started with Packwerk, first include the gem in your Gemfile. Then, generate a binstub to make the CLI accessible via bin/packwerk:

    bundle binstub packwerk

    Once the binstub is created, initialize your project to generate the necessary configuration files (packwerk.yml and the root package.yml):

    bin/packwerk init
    bundle binstub packwerk
    bin/packwerk init
  9. Set up Spring for faster Packwerk execution

    main

    Since Packwerk loads Rails, you can significantly speed up execution by using Spring.

    1. Add require 'packwerk/spring_command' to your config/spring.rb file.
    2. Run the following command to "springify" your Packwerk binstub:
    bin/spring binstub packwerk
  10. Eliminate dependency violations

    main

    Dependency violations occur when your package references a constant, class, or module defined elsewhere, but that package is not listed in your package.yml dependencies. To eliminate these violations, follow these strategies:

    1. Use or Expand the Public Interface

    • Use Existing API: Check if the target package already exposes the required functionality in its public/ folder. If so, use that public API instead of reaching into private internals.
    • Create a New Public API: If the required functionality is missing, collaborate with the owner of the target package to move that code into their public/ folder or create a new public interface.

    2. Manage Dependencies in package.yml

    • Add Explicit Dependencies: If the dependency is intentional and desired, add the target package to the dependencies key in your package's package.yml.
    • Resolve Cyclic Dependencies: If adding a dependency causes a cycle during bin/packwerk validate, work with your team to identify and remove the unnecessary link in the cycle.

    3. Redesign the System

    • Avoid the Dependency: If the dependency is unwanted, work with relevant package owners to design a solution that removes the need for that specific link.

    If none of these are possible within the scope of your current task, use bin/packwerk update-todo and provide context in your Pull Request explaining why.

  11. Upgrade from Packwerk 1.x to 2.0

    main

    When upgrading to Packwerk 2.0, several configuration changes are required to simplify setup:

    Move Gem to development group

    Packwerk no longer manages application inflections. You should move the packwerk gem from the production group to the development group in your Gemfile.

    Remove load_paths from packwerk.yml

    The load_paths key in packwerk.yml is no longer used. Packwerk now retrieves load paths directly from Rails. If using Spring, ensure it is properly configured to maintain performance.

    Revert to inflections.rb

    Packwerk no longer requires a custom inflections.yml file. You should revert to using the standard Rails config/initializers/inflections.rb file using the ActiveSupport::Inflector API.

  12. Troubleshoot Package Dependency violations

    main

    A Dependency violation occurs when a constant defined in Package A is referenced from Package B, but Package B does not list Package A as a dependency in its package.yml file.

    How to interpret the error

    When you see a violation, Packwerk provides:

    1. The file and line number where the violation occurred.
    2. The specific constant causing the violation.
    3. The package that owns the constant.
    4. The package that is missing the dependency.
    5. Inference details: The exact file path where the constant is defined.
    • Do not blindly add dependencies: If a package has declared dependencies, they were likely intentional. Adding more dependencies may increase coupling.
    • Verify package boundaries: Check if the code referencing the constant and the code defining the constant are actually in the correct packages. If they are misplaced, move the code to the appropriate package.
    • Use Dependency Inversion: If a dependency from A to B is undesirable, but B to A is acceptable, consider refactoring using dependency inversion patterns.
    • Consult domain experts: If the cause is unclear, consult with the owners/maintainers of the packages involved.
    /Users/JaneDoe/src/github.com/sample-project/billing/app/jobs/document_processing_job.rb:48:6
    Dependency violation: ::Edi::Source belongs to 'edi', but 'billing' does not specify a dependency on 'edi'.
    Are the constant and its references in the right packages?
    
    Inference details: 'Edi::Source' refers to ::Edi::Source which seems to be defined in edi/app/models/edi/source.rb.