How to automate adding plugins to all of your existing (and future) targets in a Swift package

TL;DR

package.targets = package.targets.map { target in
    var plugins = target.plugins ?? []
    plugins.append(.plugin(name: "SwiftLintPlugin", package: "SwiftLint"))
    target.plugins = plugins
    return target
}

The Problem

Last month, I was tasked with adding SwiftFormat and SwiftLint to one of the projects my team maintains. The project is medium-sized and uses Swift Package Manager (SPM) for modularization. It has a separate SPM package for each module. For example:

  • FeatureModules // Directory
    • Login // Swift Package
    • MyAccount // Swift Package
    • Dashboard // Swift Package
  • Core // Directory
    • DomainModels // Swift Package
    • Networking // Swift Package

Each module typically consists of three targets (e.g., Login, LoginTests, LoginTestUtils). The third target’s sole purpose is to avoid duplicating testing doubles. You can read more about it here.

Adding SwiftFormat to the main project and all SPM packages was pretty straightforward. I decided to integrate it at the CI level to ensure all new code changes are properly formatted. Locally, I left this choice up to the maintainers. They can use it through a post-commit git hook or as an Xcode Extension. Depending on your needs, both solutions work great.

Integrating SwiftLint into the base project was also a breeze. What became problematic was integrating it into all the Swift packages. As I mentioned before, we were using a Swift package for each module. As per the documentation:

Due to limitations with Swift Package Manager plugins, this is only recommended for projects that have a SwiftLint configuration in their root directory, as there is currently no way to pass any additional options to the SwiftLint executable.

This limitation would require us to duplicate our SwiftLint config for each package, making future updates to the SwiftLint config error-prone and time-consuming. The same goes for updating the plugin itself.

Looking For a Solution

I had two problems to solve:

  1. Automating the updating of the config file(s).
  2. Automating the addition of the SwiftLint plugin to all new Swift packages in the future.

The first problem is easily solvable with a simple bash script. It would still require writing documentation, but let’s say I’m okay with the trade-off.

The second problem got me looking at Sourcery. However, this solution seemed like overkill from the beginning. Introducing Sourcery to a project just because of SPM limitations seemed like a bad idea.

Back to square one!

Solution

I started experimenting with the project structure. Instead of creating a new Swift package for each module, I decided to create a single package that worked as a container for all existing modules. Having all the modules defined in a single Package.swift file makes automating the SwiftLint plugin trivial.

hello there

// Inject base plugins into each target
package.targets = package.targets.map { target in
    var plugins = target.plugins ?? []
    plugins.append(.plugin(name: "SwiftLintPlugin", package: "SwiftLint"))
    target.plugins = plugins
    return target
}

This approach not only solved all of my problems but also required very little work. It involved merging all package definitions into a single one and moving some files around.

This solution won’t work for larger projects. Since my app had only a handful of modules, it worked wonderfully. If you’re working on a mid-sized project with tens of packages, you might consider grouping your modules into a handful of packages like FeatureModules, Models, etc.

I hope this issue will have been resolved long before we hit the wall due to the growing number of modules!

Resources