PureXML/third_party/rules-swift/exp/when-to-create.md

5.4 KiB

When to Create a New Target

Always-loaded core. Use the decision tree first. The DO/DON'T list and the checklist below cover the corner cases.

TARGET CREATION DECISION TREE

Need to add new code?
├─ Is it a reusable domain model?
│   └─ YES → Add to SharedModels
│
├─ Is it UI-related?
│   ├─ Colors/semantic colors? → Add to AppColors
│   ├─ Fonts/typography? → Add to AppFont
│   ├─ Combined theme? → Add to AppTheme (uses AppColors + AppFont)
│   ├─ Component system infrastructure? → Add to Components (core system)
│   ├─ Hot reload support? → Add to SharedComponents
│   ├─ Reusable app component? → Add to AppComponents
│   └─ View helper/modifier? → Add to SharedViews
│
├─ Is it a complete user-facing feature?
│   └─ YES → Create new *Feature target
│       Example: ProfileFeature, SettingsFeature, PaymentFeature
│
├─ Is it API/networking related?
│   ├─ Client-side? → Add to ApiClient (or create new client target)
│   ├─ Server-side? → Add to ApiServer
│   ├─ Shared DTOs? → Add to ApiShared (OpenAPI generated)
│   └─ Middleware? → Create new *Middleware target
│
├─ Is it shared infrastructure?
│   └─ YES → Create new Shared* target
│       Example: SharedUtilities, SharedNetworking
│
└─ Still unsure?
    └─ Ask: "Could this be reused in isolation?"
        ├─ YES → Create new target
        └─ NO → Add to most specific existing target

WHEN TO CREATE A NEW TARGET

✅ DO Create New Target When:

  1. New Feature Module

    • Complete user-facing feature (login, profile, payments)
    • Example: ProfileFeature, PaymentFlowFeature
  2. Reusable Infrastructure

    • Can be tested in isolation
    • Might be used by multiple features
    • Example: CachingMiddleware, LoggingUtility
  3. Third-Party Integration

    • Wraps external library
    • Isolates external dependencies
    • Example: ApplePayIntegration, BiometricAuth
  4. Platform Separation

    • Platform-specific code (iOS vs macOS)
    • Example: IOSBiometrics, MacOSNotifications
  5. Build Optimization

    • Large, stable code that rarely changes
    • Expensive compilation (generated code)
    • Example: ApiShared (OpenAPI generated)

❌ DON'T Create New Target When:

  1. Single Use Case

    • Only used by one feature
    • Tightly coupled to specific screen
    • → Add to that feature's target
  2. Trivial Helper

    • 1-2 small functions
    • No external dependencies
    • → Add to existing utility target
  3. Temporary Code

    • Proof of concept
    • Spike/experiment
    • → Keep in feature until proven stable

DECISION CHECKLIST

Before Creating a New Target

  • Target has single, clear responsibility
  • Target name follows conventions (Feature, Shared, Api*, *Components)
  • Dependencies are minimal and explicit
  • No circular dependencies introduced
  • Can be built and tested in isolation
  • Fits into architectural layer (Foundation/Infrastructure/Feature/App)
  • Test target created alongside source target
  • Product registered in Package.swift products array
  • Imports and declared dependencies point only to lower layers (no higher-layer dependencies)

Before Adding to Existing Target

  • New code shares responsibility with the existing target
  • No better-suited target exists
  • Not creating a "god target" with mixed concerns
  • Won't introduce unwanted dependencies to target consumers

Before Modifying Package.swift

  • Used closure-with-local-variables pattern for deps
  • Used closure-with-local-variables pattern for allProducts
  • Used closure-with-local-variables pattern for targets
  • NEVER used inline array definitions
  • Grouped dependencies by purpose (OpenAPI, Vapor, Apple-only, etc.)
  • Grouped products by platform (base vs. appleOnly)
  • Grouped targets by layer (Foundation, Infrastructure, Features)
  • Used comment headers (e.g., // ---------- Shared Models ----------)
  • In Package.swift: separated platform-specific code with #if os(iOS) || os(macOS)
  • In Package.swift only: did NOT use #if canImport(UIKit) (manifest evaluates it lazily and breaks Linux CI; #if os() is manifest-parse-time and safe). In regular Swift source files, #if canImport(UIKit) / #if canImport(AppKit) is the correct idiom for platform-conditional code.
  • Each target/product/dependency has descriptive variable name
  • Used trailing commas in all arrays
  • Applied .when(platforms:) for platform-specific dependencies within targets

Before Adding Font/Resource Target

  • Used .process() for resources, NEVER .copy()
  • Created FontRegistration.swift with CoreText registration
  • Used Bundle.module for resource access, NEVER Bundle.main
  • Used #if canImport(UIKit) / #if canImport(AppKit) for platform imports
  • Filtered font files by extension (.otf, .ttf)
  • Used CTFontManagerRegisterFontsForURL with error handling
  • Added console logging for registration success/failure
  • Called FontRegistration.registerFonts() in app init BEFORE UI renders
  • NEVER used Info.plist font registration (UIAppFonts, ATSApplicationFontsPath)
  • Target has zero dependencies (fonts are Foundation layer)
  • Resources organized in dedicated subdirectory (e.g., Fonts/)