Section 4/171 menit

4. ProjectDescriptionHelpers Lanjutan

4. ProjectDescriptionHelpers Lanjutan

Layer Hierarchy untuk Tim Besar

Untuk project dengan 50+ module, ProjectDescriptionHelpers perlu diorganisasi secara hierarkis:

swift
// Tuist/ProjectDescriptionHelpers/Settings/Settings+Base.swift

import ProjectDescription

// Level 1: Base settings untuk semua targets
public extension SettingsDictionary {

    static var base: SettingsDictionary {
        [
            "SWIFT_VERSION": "6.0",
            "IPHONEOS_DEPLOYMENT_TARGET": "17.0",
            "SWIFT_STRICT_CONCURRENCY": "complete",
            "CLANG_ENABLE_OBJC_ARC": "YES",
            "ENABLE_BITCODE": "NO",
            "DEBUG_INFORMATION_FORMAT": "dwarf-with-dsym",
        ]
    }

    static var framework: SettingsDictionary {
        base.merging([
            "DEFINES_MODULE": "YES",
            "SKIP_INSTALL": "YES",
            "BUILD_LIBRARY_FOR_DISTRIBUTION": "NO",
        ])
    }

    static var appExtension: SettingsDictionary {
        base.merging([
            "APPLICATION_EXTENSION_API_ONLY": "YES",
        ])
    }
}

extension SettingsDictionary {
    // Helper untuk merge dengan override
    func merging(_ other: SettingsDictionary) -> SettingsDictionary {
        merging(other) { _, new in new }
    }
}
swift
// Tuist/ProjectDescriptionHelpers/Target/Target+Feature.swift

import ProjectDescription

public enum FeatureLayer: String {
    case core       // Foundation, networking, storage — tidak boleh depend ke feature lain
    case shared     // SharedUI, SharedModels — boleh depend ke core
    case feature    // Feature modules — boleh depend ke core dan shared
    case app        // Main app — boleh depend ke semua
}

public extension Target {

    static func make(
        name: String,
        layer: FeatureLayer,
        product: Product = .framework,
        dependencies: [TargetDependency] = [],
        resources: ResourceFileElements? = nil,
        additionalSources: [SourceFileGlob] = [],
        hasTests: Bool = true,
        testDependencies: [TargetDependency] = []
    ) -> [Target] {
        var targets: [Target] = []

        // Main target
        let mainTarget = Target.target(
            name: name,
            destinations: .iOS,
            product: product,
            bundleId: "com.company.\(name.lowercased().replacingOccurrences(of: " ", with: "."))",
            deploymentTargets: .iOS("17.0"),
            sources: SourceFilesList(
                globs: [.glob("Sources/**")] + additionalSources
            ),
            resources: resources,
            dependencies: dependencies,
            settings: .settings(
                base: layer == .app ? .base : .framework,
                configurations: makeConfigurations(for: layer)
            )
        )
        targets.append(mainTarget)

        // Test target (opsional)
        if hasTests {
            let testTarget = Target.target(
                name: "\(name)Tests",
                destinations: .iOS,
                product: .unitTests,
                bundleId: "com.company.\(name.lowercased()).tests",
                deploymentTargets: .iOS("17.0"),
                sources: "Tests/**",
                dependencies: [.target(name: name)] + testDependencies
            )
            targets.append(testTarget)
        }

        return targets
    }

    private static func makeConfigurations(for layer: FeatureLayer) -> [Configuration] {
        let debugSettings: SettingsDictionary = ["OTHER_SWIFT_FLAGS": "$(inherited) -DDEBUG"]
        let releaseSettings: SettingsDictionary = ["SWIFT_OPTIMIZATION_LEVEL": "-O"]

        return [
            .debug(name: "Debug", settings: debugSettings),
            .release(name: "Staging", settings: releaseSettings),
            .release(name: "Release", settings: releaseSettings),
        ]
    }
}
swift
// Tuist/ProjectDescriptionHelpers/Project/Project+Module.swift

import ProjectDescription

public extension Project {

    static func module(
        name: String,
        layer: FeatureLayer,
        dependencies: [TargetDependency] = [],
        resources: ResourceFileElements? = nil,
        hasExampleApp: Bool = false,
        testDependencies: [TargetDependency] = []
    ) -> Project {

        var targets = Target.make(
            name: name,
            layer: layer,
            dependencies: dependencies,
            resources: resources,
            testDependencies: testDependencies
        )

        // Example app untuk development isolation
        if hasExampleApp {
            let exampleTarget = Target.target(
                name: "\(name)Example",
                destinations: .iOS,
                product: .app,
                bundleId: "com.company.\(name.lowercased()).example",
                deploymentTargets: .iOS("17.0"),
                infoPlist: .default,
                sources: "Example/Sources/**",
                resources: "Example/Resources/**",
                dependencies: [.target(name: name)]
            )
            targets.append(exampleTarget)
        }

        return Project(
            name: name,
            organizationName: "Company",
            options: .options(
                automaticSchemesOptions: .enabled(
                    targetSchemesGrouping: .singleScheme
                )
            ),
            targets: targets
        )
    }
}

Menggunakan Helper

swift
// Projects/Features/FeaturePayment/Project.swift

import ProjectDescription
import ProjectDescriptionHelpers

let project = Project.module(
    name: "FeaturePayment",
    layer: .feature,
    dependencies: [
        .project(target: "NetworkKit", path: "../../Core/NetworkKit"),
        .project(target: "SharedUI", path: "../../Shared/SharedUI"),
        .external(name: "Stripe"),
    ],
    hasExampleApp: true,
    testDependencies: [
        .external(name: "Quick"),
        .external(name: "Nimble"),
    ]
)
// Cukup 15 baris untuk mendefinisikan module lengkap dengan test target dan example app

Enforcing Architecture Rules via Helpers

swift
// Tuist/ProjectDescriptionHelpers/Architecture/ArchitectureLayer.swift

import ProjectDescription

// Enforce bahwa Core layer tidak boleh depend ke Feature layer
public extension TargetDependency {

    // Tipe-safe dependency yang mencegah salah layer
    static func core(_ name: String) -> TargetDependency {
        .project(
            target: name,
            path: .relativeToManifest("../../Core/\(name)")
        )
    }

    static func shared(_ name: String) -> TargetDependency {
        .project(
            target: name,
            path: .relativeToManifest("../../Shared/\(name)")
        )
    }

    static func feature(_ name: String) -> TargetDependency {
        .project(
            target: name,
            path: .relativeToManifest("../../Features/\(name)")
        )
    }
}

// Usage — lebih ekspresif dan path-safe:
// dependencies: [.core("NetworkKit"), .shared("SharedUI")]
// vs sebelumnya:
// dependencies: [.project(target: "NetworkKit", path: "../../Core/NetworkKit")]