Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ let package = Package(
dependencies: [
.package(
url: "https://github.com/tuist/GraphViz.git",
branch: "main" // a few commits ahead of the deprecated GraphViz original repo. It also includes Xcode 16 fixes.
// a few commits ahead of the deprecated GraphViz original repo. It also includes Xcode 16 fixes.
revision: "083bccf9e492fd5731dd288a46741ea80148f508"
),
.package(
url: "https://github.com/apple/swift-argument-parser.git",
Expand Down
105 changes: 104 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,104 @@
# spmgraph
# spmgraph - SwiftPM dependency graph management

A CLI tool that **unlocks Swift dependency graphs**, giving you extra information and capabilities.
With it you can visualize your dependency graph, run selective testing, and enforce architectural rules for optimal modular setups.

## How to use

`spmgraph` is run for a local `Package.swift` and takes the user configuration from a `SPMGraphConfig.swift` file.

### Edit
Create and edit your spmgraph config

- run `spmgraph edit <'Package.swift' path> <options>`
- it creates an initial `SPMGraphConfig.swift` on the same path as your `Package.swift`
- spmgraph opens up a temporary Swift Package where you configure spmgraph in Swift and build to check that everything is correct

### Visualize
Generate an image with a visual representation of your dependency. **Open the map!**

run `spmgraph visualize <'Package.swift' path> help` for more

### Tests
**Selective testing** based on git changes or a given list of changed files.
The output is a comma separate list of test targets that can be fed into `xcodebuild`'s `-only-testing:TEST-IDENTIFIER` or [fastlane scan](https://docs.fastlane.tools/actions/scan/#scan)'s `only_testing`

run `spmgraph tests <'Package.swift' path> help` for more

### Lint
Verify if the dependency graph follow the defined team and industry best practices.

For example, enforce that
- feature modules don't depend on each other
- Linked dependencies are imported (used) at least once
- Base modules don't depend on feature modules
- the dependency graph isn't too deep

All possible using Swift. Below an example of creating your own lint rule by traversing the dependency graph:
```swift
extension SPMGraphConfig.Lint.Rule {
static let unregisteredLiveModules = Self(
id: "unregisteredLiveModules",
name: "Unregistered Live modules",
abstract: "Live modules need to be added to the app target / feature module as dependencies.",
validate: { package, excludedSuffixes in
let liveModules = package
.modules
.compactMap { module -> Module? in
guard !module.containsOneOf(suffixes: excludedSuffixes), module.isLiveModule else {
return nil
}
return module
}

guard
let featureModule = package
.modules
.first(where: { $0.name == "GetYourGuideFeature" }),
case let featureModuleDependencies = featureModule
.dependencies
.compactMap(\.module)
else {
return [LintError.missingFeatureModule]
}

return liveModules.compactMap { liveModule in
if !featureModuleDependencies.contains(liveModule) {
return LintError.unregisteredLiveModules(
moduleName: liveModule.name,
appModule: featureModule.name
)
}

return nil
}
}
)
}
```

run `spmgraph lint <'Package.swift' path> help` for more

## Requirements
- [graphviz](https://github.com/graphp/graphviz) (available via `brew install graphviz`)
- Xcode 16+ and the Swift 6.0+ toolchain

## Installation

### [Mint](https://github.com/yonaskolb/mint)

```
mint install getyourguide/spmgraph
```
* For optimal build times make sure `~/.mint/bin/spmgraph` is cached on your CI runner.

## Acknowledgments
- Inspired by the work that the [Tuist](https://tuist.dev/) team does for the Apple developers community and their focus on leveraging the dependency graph to provide amazing features for engineers. Also, source of inspiration for our shell abstraction layer.

## Open roadmap
- [ ] Cover the core logic of Lint, Map and Visualize libs with tests
- [ ] Support macros (to become a GitHub issue)

Ideas
- [ ] Lint - see if it can be improved to cover auto-exported dependencies. For example usages of `import Dependencies` justify linking `DependenciesExtras` as a dependency.
- [ ] Add fix it suggestion to lint errors
19 changes: 13 additions & 6 deletions Sources/SPMGraphConfigSetup/Resources/SPMGraphConfig.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import SPMGraphDescriptionInterface

// MARK: - Instructions

// You can leverage the minimal and advanced configurations below.
// You can leverage the default, minimal and advanced configurations below.
//
// By default, when using the minimal config, the lint rules are the `.default` ones included
// in SPMGraphConfig.
// When using either the `default` or the `minimal` configurations, the lint rules are the ones
// that are default and built-in.
//
// The advanced configuration contains examples of creating our own dependency graph rules,
// including how to extend the PackageModel types with helpers that, for example, categorize types
Expand All @@ -18,14 +18,21 @@ import SPMGraphDescriptionInterface
//
// Have fun! Ah, and don't forget to select `macOS` as the build target ;)!

// MARK: - Default

// the quickest path in case you don't care about the strict mode and custom lint rules
let spmGraphConfig = SPMGraphConfig.default

// MARK: - Minimal

//let spmGraphConfig = SPMGraphConfig(
// lint: SPMGraphConfig.Lint(isStrict: true)
//)
// the default lint rules and strict mode enabled to fail on lint errors
let spmGraphConfig = SPMGraphConfig(
lint: SPMGraphConfig.Lint(isStrict: true)
)

// MARK: - Advanced

// custom configuration, with both default and team specific graph lint rules
let spmGraphConfig = SPMGraphConfig(
lint: SPMGraphConfig.Lint(
rules: .custom + .default, // The default rules + your custom ones
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ public struct SPMGraphConfig: Sendable {
self.isStrict = isStrict
self.expectedWarningsCount = expectedWarningsCount
}

public static let `default`: Self = .init(isStrict: false)
}

/// Configuration for selective testing.
Expand Down Expand Up @@ -110,6 +112,9 @@ public struct SPMGraphConfig: Sendable {
self.tests = tests
self.excludedSuffixes = excludedSuffixes
}

/// The default ``SPMGraphConfig`` with strict mode disabled and the built-in lint rules.
public static let `default`: Self = .init(lint: .default)
}

typealias Validate = (Package, _ excludedSuffixes: [String]) -> [LocalizedError]
Expand Down
3 changes: 1 addition & 2 deletions Sources/SPMGraphExecutable/Subcommands/Edit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ struct Edit: AsyncParsableCommand {
discussion: """
It looks for an `SPMGraphConfig.swift` file in the same directory as the `Package.swift` under analyzes. If there's none, it creates a fresh one from a template.

Next, it generates a temporary package for editing your `SPMGraphConfig.swift`, where you customize multiple settings, from the expected warnings count to
writing your own dependency graph rules in Swift code.
Next, it generates a temporary package for editing your `SPMGraphConfig.swift`, where you customize multiple settings, from the expected warnings count to writing your own dependency graph rules in Swift code.

Once the `SPMGraphConfig.swift` is edited, your configuration is dynamic loaded into spmgraph and leveraged on all other commands.
""",
Expand Down