PureXML/third_party/rules-swift/exp/package-swift.md
Mihaela Mihaljevic c7ef8abfd9 chore(rules): pin rules-swift corpus, add gates and CI; drop redundant docs/rules mirror
Adopt the rules-swift pin-and-codify strategy in snapshot mode (third_party/rules-swift
@97dc70f + sha256), add codified gates, a conformance audit ledger, and
.github/workflows/rules-swift.yml (corpus-pin integrity required, full gate report mode).
Remove the docs/rules mirror it supersedes (repo-specific files kept under docs/). Style
and leak gates exclude the vendored corpus.
2026-06-30 14:02:40 +02:00

8.8 KiB

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

// ---------- 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

// ---------- 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.

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:
grep -nE 'let (deps|allProducts|targets)[^=]*=[[:space:]]*\[' Packages/Package.swift

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: 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.