Package Generator is a Swift Package Manager Plugin for simply updating your Package.swift file consistently and understandably. This is a great tool for projects that are heavily modularized or use TCA and thus rely on a clean and updated Package.swift.
Package Generator adds imports that it read from the source code files to their target in Package.swift. This will help reduce compilation issues with SwiftUI Preview too.
After installing it you will be able to run it but for it to work properly it needs to be configured. By default, it will run with dry-run set to true and this will create a file Package_generated.swift to allow you to preview what will happen. After having properly configured it and testing that the Package_generated.swift generate the correct content you will need to set dry-run to false in the configuration to write in the real Package.swift file.
Each time you need to add a module remember to add it to the configuration file.
Package Generator goes to all folders set in the configuration then read all swift files to look at all the imports to create a target to add to the Package.swift.
The code analyzing part is made using swift-syntax since I didn't find a way to link it to the plugin I have to package it in a CLI that is used to do the parsing part.
Add to your dependencies .package(url: "https://github.com/mackoj/PackageGeneratorPlugin.git", from: "0.5.0"),
The plugin will display messages and errors in Xcode Report navigator.
By default to prevent any surprise it will do a dry-run(not modifying your Package.swift but creating a Package_generated.swift) for you to allow you time to review it before using it.
To use it you have to set a configuration file at the root of your project named packageGenerator.json.
This file contains these keys:
packageDirectories: An array of string that represents where the modules areheaderFileURL: A string that represents the path of the file that will be copied at the top of thePackage.swiftspaces: An int that represents the number of spaces that thePackage.swiftgenerator should use when adding contentverbose: A bool that represents if it should print more information in the consolepragmaMark: A bool that represents if we should add// MARK: -in the generated filedryRun: A bool that represents if the generator should replace thePackage.swiftfile or create aPackage_generated.swiftmappers.targets: An dictionary that handles target renaming the key represents a targetpathwith the/and the value represents the name to apply. For example in thepackageDirectoriesI haveSources/App/Helpers/Foundationbut in my code, I importFoundationHelpers.mappers.imports: An dictionary that represents how to map import that requires a.productin SPM for exampleComposableArchitecturerequire to be called.product(name: "ComposableArchitecture", package: "swift-composable-architecture")in aPackage.swift.exclusions: An object that represents all imports that should not be added as dependencies to a target or targets in the generatedPackage.swiftexclusions.apple: An array of string that represents all Apple SDK that should not be add as dependencies to a targetexclusions.imports: An array of string that represents all other SDK that should not be added as dependencies to a targetexclusions.targets: An array of string that represent all targets that should not be added in the generatedPackage.swifttargetsParameters: An dictionary that represent what custom parameter to add to a targetgenerateExportedFiles: A bool that represents if the generator should createexported.swiftfiles in each package with@_exported importstatements for local dependenciesexportedFilesRelativePath: An optional string that specifies a relative path within each package where theexported.swiftfiles should be placed. If not specified, files are placed in the package root directory.
{
"packageDirectories": [
"Sources/App/Clients/Analytics",
"Sources/App/Clients/AnalyticsLive",
"Sources/App/Daemons/Notification",
"Sources/App/Helpers/Foundation"
],
"headerFileURL": "header.swift",
"targetsParameters": {
"Analytics": ["exclude: [\"__Snapshots__\"]", "resources: [.copy(\"Fonts/\")]"],
"target2": ["resources: [.copy(\"Dictionaries/\")]"]
},
"verbose": false,
"pragmaMark": false,
"spaces": 2,
"dryRun": true,
"generateExportedFiles": false,
"exportedFilesRelativePath": "Generated",
"mappers": {
"targets": {
"Sources/App/Helpers/Foundation/": "FoundationHelpers",
},
"imports": {
"ComposableArchitecture": ".product(name: \"ComposableArchitecture\", package: \"swift-composable-architecture\")"
}
},
"exclusions": {
"apple": [
"ARKit",
"AVFoundation"
],
"imports": [
"PurchasesCoreSwift"
],
"targets": [
"ParserCLI"
]
}
}If a new configuration filename is used as explained in #basic-usage step 1. It will be saved so that you will not be required to input the configuration fileName at each launch.
The content of headerFileURL from the configuration will be added to the top of the generated Package.swift.
I advise adding all required dependencies and Test Targets, System Librarys, Executable Targets and Binary Targets(#8).
// swift-tools-version:5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
import Foundation
import PackageDescription
var package = Package(
name: "project",
defaultLocalization: "en",
platforms: [
.macOS(.v12),
.iOS("15.0")
],
products: [
.executable(name: "server", targets: ["server"]),
.executable(name: "parse", targets: ["ParserRunner"]),
],
dependencies: [
.package(url: "https://github.com/mackoj/PackageGeneratorPlugin.git", from: "0.3.0"),
.package(url: "https://github.com/mackoj/SchemeGeneratorPlugin.git", from: "0.5.5"),
.package(url: "https://github.com/pointfreeco/swift-composable-architecture.git", from: "0.45.0"),
],
targets: [
// MARK: -
// MARK: Test Targets
.testTarget(
name: "MyProjectTests",
dependencies: [
"MyProject",
]
),
// MARK: -
// MARK: Executables
.executableTarget(
name: "server",
path: "Sources/Backend/Sources/Run"
),
.executableTarget(
name: "ParserRunner",
path: "Sources/App/Parsers/Runner"
),
]
)When generateExportedFiles is set to true in the configuration, the plugin will generate an exported.swift file in each package directory that contains @_exported import statements for all local dependencies of that package.
This improves developer experience by automatically re-exporting local dependencies, so users don't need to manually import all the dependencies they need in their code.
For example, if package MyPackage1 depends on Chip and Logger, the generated exported.swift will contain:
// This file is auto-generated by PackageGeneratorPlugin
// It exports all local dependencies for this package
@_exported import Chip
@_exported import LoggerIn dry run mode, the files will be named exported_generated.swift to allow you to review the output before enabling the feature.
You can customize where the exported files are placed within each package by setting the exportedFilesRelativePath parameter. This allows for better organization of your generated files.
- If not specified (or
null), exported files are placed directly in each package's root directory - If specified (e.g.,
"Generated"), exported files are placed in the specified subdirectory within each package
For example, with "exportedFilesRelativePath": "Generated", the exported.swift file for a package at Sources/MyPackage/ would be created at Sources/MyPackage/Generated/exported.swift.
You can use it in CI to automatically generate your Package.swift.
swift package plugin --allow-writing-to-package-directory package-generator
Why is the plug-in is not visible in Xcode?
Plug-in can work if you do a right click on your project package and only if the Resolves Packages is passing without issue.
Why does the plugin have an executable dependency?
Because we cannot import other packages in an SPM Plugin and we need swift-syntax to parse code and extract imports.
It always creates an invalid
Package.swiftfile.
Look at the Report Navigator in Xcode it might be due to imports that don't exist or that require the use of mappers-imports.
Why doesn't it use a hidden file like
.packageGeneratorfor configuring the tool?
Because it would not be visible in Xcode and this file might need to be edited often. But you can change this if you want by giving the --confFile argument when using the tool.


