PureXML/third_party/rules-swift/exp/package-swift.md

248 lines
8.8 KiB
Markdown

# Package.swift Patterns
The closure-with-local-variables pattern for the three top-level arrays in `Package.swift`: `deps`, `allProducts`, `targets`.
All top-level `Package.swift` arrays (`deps`, `allProducts`, `targets`) use the closure-with-local-variables pattern.
### Dependencies Definition
```swift
// ---------- Dependencies ----------
let deps: [Package.Dependency] = {
// Apple's OpenAPI stack
let openAPIGeneratorDep = Package.Dependency.package(
url: "https://github.com/apple/swift-openapi-generator",
from: "1.10.3"
)
let openAPIRuntimeDep = Package.Dependency.package(
url: "https://github.com/apple/swift-openapi-runtime",
from: "1.8.3"
)
let openAPIVaporDep = Package.Dependency.package(
url: "https://github.com/swift-server/swift-openapi-vapor",
from: "1.0.1"
)
// Vapor stack
let vaporDep = Package.Dependency.package(
url: "https://github.com/vapor/vapor",
from: "4.119.0"
)
let fluentDep = Package.Dependency.package(
url: "https://github.com/vapor/fluent",
from: "4.13.0"
)
let fluentSQLiteDep = Package.Dependency.package(
url: "https://github.com/vapor/fluent-sqlite-driver",
from: "4.8.1"
)
// Custom middlewares
let loggingMiddlewareDep = Package.Dependency.package(
url: "https://github.com/mihaelamj/OpenAPILoggingMiddleware",
from: "1.1.0"
)
let bearerTokenDep = Package.Dependency.package(
url: "https://github.com/mihaelamj/BearerTokenAuthMiddleware",
from: "1.2.0"
)
// Apple-only dependencies (safe on Linux CI - only used by Apple targets)
let fileWatchersDep = Package.Dependency.package(
url: "https://github.com/krzysztofzablocki/KZFileWatchers.git",
from: "1.0.0"
)
let injectDep = Package.Dependency.package(
url: "https://github.com/krzysztofzablocki/Inject.git",
from: "1.2.4"
)
let playbookDep = Package.Dependency.package(
url: "https://github.com/playbook-ui/playbook-ios",
from: "0.4.0"
)
return [
openAPIGeneratorDep,
openAPIRuntimeDep,
openAPIVaporDep,
vaporDep,
fluentDep,
fluentSQLiteDep,
loggingMiddlewareDep,
bearerTokenDep,
fileWatchersDep,
injectDep,
playbookDep,
]
}()
```
Group dependencies by purpose (OpenAPI, Vapor, Middlewares, Apple-only), use descriptive variable names.
### Product Definition
```swift
// ---------- Products ----------
let allProducts: [Product] = {
// Base products (all platforms)
let apiSharedProduct = Product.singleTargetLibrary("ApiShared")
let apiServerProduct = Product.singleTargetLibrary("ApiServer")
let apiClientProduct = Product.singleTargetLibrary("ApiClient")
let sharedModelsProduct = Product.singleTargetLibrary("SharedModels")
let cachingMiddlewareProduct = Product.singleTargetLibrary("OpenAPICachingMiddleware")
let serverAppProduct = Product.executable(name: "apiserverapp", targets: ["ApiServerApp"])
let baseProducts: [Product] = [
apiSharedProduct,
apiServerProduct,
apiClientProduct,
sharedModelsProduct,
cachingMiddlewareProduct,
serverAppProduct,
]
// Apple-only products (iOS + macOS)
#if os(iOS) || os(macOS)
let appColorsProduct = Product.singleTargetLibrary("AppColors")
let appThemeProduct = Product.singleTargetLibrary("AppTheme")
let sharedViewsProduct = Product.singleTargetLibrary("SharedViews")
let authFeatureProduct = Product.singleTargetLibrary("AuthFeature")
let appFeatureProduct = Product.singleTargetLibrary("AppFeature")
let componentsProduct = Product.singleTargetLibrary("Components")
let appComponentsProduct = Product.singleTargetLibrary("AppComponents")
let appleOnlyProducts: [Product] = [
appColorsProduct,
appThemeProduct,
sharedViewsProduct,
authFeatureProduct,
appFeatureProduct,
componentsProduct,
appComponentsProduct,
]
#else
let appleOnlyProducts: [Product] = []
#endif
// Always exposed (for Xcode scheme visibility)
let playbookProduct = Product.singleTargetLibrary("PlaybookFeature")
return baseProducts + appleOnlyProducts + [playbookProduct]
}()
// Helper extension
extension Product {
static func singleTargetLibrary(_ name: String) -> Product {
.library(name: name, targets: [name])
}
}
```
Use local variables for each product, group by platform, use helper extensions for common patterns.
### Target Organization
Declare targets as individual variables inside a closure, group by layer, then return their concatenation.
```swift
let targets: [Target] = {
// ---------- Shared Models ----------
let sharedModelsTarget = Target.target(
name: "SharedModels",
dependencies: []
)
let sharedModelsTestsTarget = Target.testTarget(
name: "SharedModelsTests",
dependencies: ["SharedModels"]
)
let modelTargets = [
sharedModelsTarget,
sharedModelsTestsTarget,
]
// ---------- API Layer ----------
let apiSharedTarget = Target.target(
name: "ApiShared",
dependencies: [
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
],
plugins: [
.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"),
]
)
let apiSharedTestsTarget = Target.testTarget(
name: "ApiSharedTests",
dependencies: ["ApiShared"]
)
let apiClientTarget = Target.target(
name: "ApiClient",
dependencies: [
"ApiShared",
"SharedModels",
]
)
let apiClientTestsTarget = Target.testTarget(
name: "ApiClientTests",
dependencies: ["ApiClient"]
)
let apiTargets = [
apiSharedTarget,
apiSharedTestsTarget,
apiClientTarget,
apiClientTestsTarget,
]
// ---------- UI Components ----------
let appThemeTarget = Target.target(
name: "AppTheme",
dependencies: []
)
let sharedViewsTarget = Target.target(
name: "SharedViews",
dependencies: [
"AppTheme",
"AppFont",
]
)
let uiTargets = [
appThemeTarget,
sharedViewsTarget,
]
// Return all targets grouped by layer
return modelTargets + apiTargets + uiTargets
}()
```
**Why this pattern:**
- ✅ Clear visual separation with comment headers (e.g., `// ---------- Shared Models ----------`)
- ✅ Each target has a descriptive variable name (`sharedModelsTarget`, not inline Target.target(...))
- ✅ Easy to reference targets within Package.swift (can reuse variable names)
- ✅ Groups targets by layer/domain for better organization
- ✅ Trailing commas in arrays for cleaner diffs
- ✅ Makes Package.swift more maintainable as project grows
Don't define targets inline in the array; use intermediate variables.
## Acceptance check
Conforms when the manifest's three top-level arrays are built the closure-with-local-variables way, not as inline literals:
- **Each array opens a closure, not a bracket.** `deps`, `allProducts`, and `targets` read `let <name>: [...] = {` and return their grouped concatenation:
```bash
MANIFEST=Package.swift
[ -f "$MANIFEST" ] || MANIFEST=Packages/Package.swift
grep -nE 'let (deps|allProducts|targets)[^=]*=[[:space:]]*\[' "$MANIFEST"
```
Output MUST be empty. A hit means an array was written as an inline `[...]` literal, the anti-pattern this file replaces.
- **No targets defined inline in the returned array.** Each target is a named intermediate variable (`let sharedModelsTarget = Target.target(...)`), grouped by layer, then concatenated in the `return`. Confirm by reading the `targets` closure: the `return` line is a sum of group arrays (`return modelTargets + apiTargets + ...`), not a literal full of `Target.target(...)` calls.
- **Observable checklist**, each a clear pass/fail: comment headers separate the groups (`// ---------- Shared Models ----------`); every variable has a descriptive name rather than an inline expression; and arrays use trailing commas for clean diffs. A manifest whose `deps`, `allProducts`, or `targets` is a bare inline array fails this rule even if it compiles.
## See also
- [`core/folder-grouping.md`](../core/folder-grouping.md): when many single-file targets share a semantic kind (e.g. 27 importers, 5 renderers), they can colocate under a single parent folder using `path:` + disjoint `sources:` declarations. A `flatImporter(_:in:dependencies:)` helper is the worked example: 26 single-file importer targets share `Sources/Import/Importers/<cluster>/` paths while remaining independent SPM targets. The closure-with-local-variables pattern above still applies; the helper just returns a `Target.target(...)` with the path/sources baked in.