PackageGenerator CLI parses every Swift file of a package with swift-syntax and reports, per target,
the modules it imports. It is the engine of the
PackageGenerator plugin, which turns that report
into your Package.swift, so a 200-target app never has a dependency list that lies.
# What the plugin runs for you package-generator-cli \ --input-file-url targets.json \ --output-file-url imports.json \ --package-directory . --verbose [package-generator-cli] Processing 212 targets [package-generator-cli] Processing target "Checkout" [package-generator-cli] Processing test module "CheckoutTests" … [package-generator-cli] Finished: 398 packages written, elapsed: 0.03 seconds
The problem
In a modular app, every feature, client and helper is its own SwiftPM target. Someone adds
import Analytics and forgets the manifest; someone removes the last use of Networking
and the dependency stays forever. Builds break in CI, or worse, keep working with a graph nobody can reason about.
The source of truth is already in your files: the imports. PackageGenerator reads them and writes the manifest. This CLI is the part that reads them, fast enough to run on every change.
How it works
You list target folders once in packageGenerator.yaml.
The plugin resolves paths and writes a JSON list of targets, tests and excludes.
PluginThe CLI parses each target's Swift files concurrently and writes each target's imports as JSON.
This CLIThe plugin drops Apple SDKs, maps imports to targets and products, and writes the manifest.
PluginThe contract
The input names each target, its optional test target and paths to exclude. A bare path string, the
original format, still works. The output lists each target's imported modules, sorted and deduplicated, with the
target's path and its Resources folder if it has one. Both types ship in the
PackageGeneratorModels library.
[
{
"target": { "path": "/App/Sources/Checkout", "name": "Checkout" },
"test": {
"path": "/App/Tests/CheckoutTests",
"name": "CheckoutTests",
"exclude": ["__Snapshots__"]
}
},
"/App/Sources/Legacy"
]
[
{
"name": "Checkout",
"isTest": false,
"dependencies": ["Analytics", "Foundation", "Payments", "SwiftUI"],
"path": "Sources/Checkout",
"fullPath": "/App/Sources/Checkout",
"resources": "Resources",
"localDependencies": 0,
"hasBiggestNumberOfDependencies": false
},
…
]
What it reads
No regular expressions over your code: swift-syntax builds the syntax tree, so comments, strings and odd formatting never fool it.
@testable, @_exported,
import struct Foundation.URL and imports inside #if canImport(…) all count, by their
top-level module.
public, internal and
package imports are dependencies; private and fileprivate ones are not.
A file that imports its own module never turns into a target depending on itself.
Paths are excluded relative to the target, the package or
absolutely; a Resources folder is reported for the plugin to declare.
Targets and files are processed in task groups with a sliding window of twice the core count, under Swift 6 strict concurrency.
stdout stays empty; --verbose prints progress to stderr,
and diagnostics go to os_log under com.mackoj.PackageGeneratorCLI.
Performance
Imports live at the top of a file, so the scanner finds the end of the last import declaration and parses only that much. Across 23,000 open-source Swift files it parses 9% of the bytes and finds exactly the imports a full parse finds.
| Benchmark | Before | After | Mallocs |
|---|---|---|---|
| Scan a package, 2,040 files in 80 targets | 767 ms | 29 ms 26.7x | 1.53M → 154k |
| Scan one target, 500 files | 286 ms | 10 ms 27.7x | 556k → 50k |
| Parse 100 files, one at a time | 92 ms | 4.2 ms 21.7x | 73.6k → 6.1k |
| Decode 2,000 input targets | 11.4 ms | 8.2 ms | 44.3k → 39.9k |
p50 on an Apple silicon Mac, measured with package-benchmark on a synthetic package. Method and history: docs/performance.md.
Install
Add the plugin and it downloads the CLI as a prebuilt artifact bundle. Reach for the CLI directly only to script it or to build your own tooling on its JSON.
In your root Package.swift, then right-click the package in Xcode, or run it from CI.
.package( url: "https://github.com/mackoj/PackageGeneratorPlugin.git", from: "1.0.6" )
swift package plugin \ --allow-writing-to-package-directory package-generator
Download the artifact bundle from the latest release, or build it from source.
git clone https://github.com/mackoj/PackageGeneratorCLI.git cd PackageGeneratorCLI swift build -c release .build/release/package-generator-cli --help
// .product(name: "PackageGeneratorModels", package: "PackageGeneratorCLI") import Foundation import PackageGeneratorModels let output = URL(fileURLWithPath: "imports.json") let packages = try JSONDecoder().decode( [ParsedPackage].self, from: Data(contentsOf: output)) for package in packages where !package.isTest { print(package.name, package.dependencies) }