Jonasfj.dk/Blog
A blog by Jonas Finnemann Jensen


May 14, 2022
Vendoring Dart Dependencies
Filed under: Computer,Dart,English,Google by jonasfj at 7:54 pm

Disclaimer: The opinions stated here are my own, not necessarily those of my employer.

When resolving dependencies for a Dart or Flutter application it is only possible to have one version of a given package. This ensures that the dependency graph is fairly lean, since each package can only be in the graph ones. So unlike npm, you won’t end up with 3 different versions of some utility package.

On the flip side, you can run into dependency conflicts. If two packages have dependency on foo, then they cannot be used together unless there a version of foo that satisfies both constraints. As most package authors are using caret-constraints (foo: ^1.2.0) this mostly becomes a problem a new major version of foo is published. For example, pkg_a might require foo: ^1.0.0, while pkg_b requires foo: ^2.0.0, effectively preventing packages pkg_a and pkg_b from being used in the same application.

There are some workarounds for these scenarios:

  • Author of pkg_a can publish a new version that is compatible with foo: ^2.0.0.
  • If version 1.0.0 and 2.0.0 of foo are similar, it may be possible for the author of pkg_b to publish a version compatible with both using a constraint like foo: >=1.0.0 <3.0.0.
  • If an older version of pkg_b is compatible with foo: ^1.0.0, the user may choose to use an older version of pkg_b. Indeed the solver will pick an older verison of pkg_b, if allowed by dependency constraints.
  • If version 1.0.0 and 2.0.0 of foo are sufficiently similar, it may be possible for the user to employ dependency_overrides to circumvent the dependency-conflict.

However, these workarounds are not always possible. The author of pkg_a might have abandoned the package. Changes between 1.0.0 and 2.0.0 of foo might be so breaking that using a version range or dependency_overrides is infeasible. It may be that there is some functionality in foo version 1.0.0 that is not in 2.0.0 and it is critical for pkg_a to work.

At some point, you may reach the conclusion that it would better to have multiple versions of foo than finding a clean way to fix the underlying issue. In such scenarios, package:vendor can help you vendor dependencies.

Vendoring Dependencies

To vendor dependencies with package:vendor you will need a vendor.yaml file next to your pubspec.yaml. The vendor.yaml file allows you to specify:

  • Which package and version to vendor.
  • Under what folder name a package version should be vendored.
  • What import-rewrites to apply to your application code.
  • What import-rewrites to apply to the vendored packages.
  • Which files to vendor for a given package.

The vendor.yaml file looks as follows:

# vendor.yaml
import_rewrites:
  # Map of rewrites that should be applied to lib/, bin/ and test/
  # Example:
  #   import "package:<package>/..."
  # is rewritten to:
  #   import "package:myapp/src/third_party/<name>/lib/..."
  # where 'myapp' is the root project name.
  <package>: <name>
vendored_dependencies:
  # Specification of which packages to be vendor into:
  #   lib/src/third_party/<name>/
  <name>:
    package: <package>
    version: <version>
    import_rewrites:
      # Rewrites to be applied inside: lib/src/third_party/<name>/
      <package>: <name>
    include:
    # Glob patterns for which files to include from the package.
    # For syntax documentation see: https://pub.dev/packages/glob
    #
    # If not specified `include` will default to the following list:
    - pubspec.yaml # always renamed vendored-pubspec.yaml
    - README.md
    - LICENSE
    - CHANGELOG.md
    - lib/**
    - analysis_options.yaml

In effect, the vendor.yaml specifies the desired state after vendoring packages. When creating a vendor.yaml file it is your responsibility that the dependencies of the package you’re vendoring are satisfied. You can do this by adding them as a dependency in your pubspec.yaml, or by vendoring the dependency and applying an import_rewrite to the original vendored package.

When you’ve written a vendor.yaml you can do dart run vendor to vendor dependencies. Vendored dependencies are written to lib/src/third_party/<name>/, and their public libraries must usually be imported from lib/src/third_party/<name>/lib/<library>.dart.

Once you have vendored dependencies it is advicable that you commit your vendor.yaml and the contents of lib/src/third_party/. The dart pub vendor command will store the current state of vendored dependencies in lib/src/third_party/vendor-state.yaml. This allows dart pub vendor to detect the changes in your vendor.yaml file. Thus, when you run dart pub vendor it will not delete vendored packages that have no changed. This is important as it allows you to patch the packages you have vendored.

Hence, you can also use package:vendor, if you need to make a quick modification of a dependency. But if you patch your vendored dependencies, you should take care to run dart pub vendor --dry run next time your vendoring. This helps you check if you’re deleting the changes you’ve made.

All in all, I think package:vendor turned out as a nice solution. It’s not a fix for everything. And if the dependency you’re vendoring integrates with a lot of other packages it can quickly get messy, and you might have to vendor a lot of dependencies to get things working. Vendoring is not a solution to every problem, but it is another tool for resolving dependency conflicts, or easily patching that one dependency that doesn’t get updated anymore.

In general, we should all hope we’ll never have to vendor dependencies.