• swift-syntax
  • Swift 6, concurrent
  • macOS 13+
  • Artifact bundle

Your imports already say what your targets depend on.

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.

Terminal
# 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

Hand-written dependency lists drift.

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

Plugin and CLI, one pass.

  1. Describe targets

    You list target folders once in packageGenerator.yaml.

    Plugin
  2. Hand them over

    The plugin resolves paths and writes a JSON list of targets, tests and excludes.

    Plugin
  3. Scan every file

    The CLI parses each target's Swift files concurrently and writes each target's imports as JSON.

    This CLI
  4. Write Package.swift

    The plugin drops Apple SDKs, maps imports to targets and products, and writes the manifest.

    Plugin

The contract

JSON in, JSON out.

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.

targets.json (input)
[
  {
    "target": { "path": "/App/Sources/Checkout", "name": "Checkout" },
    "test": {
      "path": "/App/Tests/CheckoutTests",
      "name": "CheckoutTests",
      "exclude": ["__Snapshots__"]
    }
  },
  "/App/Sources/Legacy"
]
imports.json (output)
[
  {
    "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

Real Swift, parsed like the compiler does.

No regular expressions over your code: swift-syntax builds the syntax tree, so comments, strings and odd formatting never fool it.

Every import form

@testable, @_exported, import struct Foundation.URL and imports inside #if canImport(…) all count, by their top-level module.

Access levels honoured

public, internal and package imports are dependencies; private and fileprivate ones are not.

Self-imports dropped

A file that imports its own module never turns into a target depending on itself.

Excludes and Resources

Paths are excluded relative to the target, the package or absolutely; a Resources folder is reported for the plugin to declare.

Concurrent and bounded

Targets and files are processed in task groups with a sliding window of twice the core count, under Swift 6 strict concurrency.

Quiet by design

stdout stays empty; --verbose prints progress to stderr, and diagnostics go to os_log under com.mackoj.PackageGeneratorCLI.

Performance

Reads only what it needs.

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.

BenchmarkBeforeAfterMallocs
Scan a package, 2,040 files in 80 targets767 ms29 ms 26.7x1.53M → 154k
Scan one target, 500 files286 ms10 ms 27.7x556k → 50k
Parse 100 files, one at a time92 ms4.2 ms 21.7x73.6k → 6.1k
Decode 2,000 input targets11.4 ms8.2 ms44.3k → 39.9k

p50 on an Apple silicon Mac, measured with package-benchmark on a synthetic package. Method and history: docs/performance.md.

Install

Most people never install it.

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.

The plugin (recommended)

In your root Package.swift, then right-click the package in Xcode, or run it from CI.

Package.swift
.package(
  url: "https://github.com/mackoj/PackageGeneratorPlugin.git",
  from: "1.0.6"
)
Terminal
swift package plugin \
  --allow-writing-to-package-directory package-generator

The CLI on its own

Download the artifact bundle from the latest release, or build it from source.

Terminal
git clone https://github.com/mackoj/PackageGeneratorCLI.git
cd PackageGeneratorCLI
swift build -c release
.build/release/package-generator-cli --help
Decode the output in Swift
// .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)
}