Section 9/171 menit

9. Module Graph Analysis dan Enforcement

9. Module Graph Analysis dan Enforcement

Visualisasi dengan tuist graph

swift
# Generate dependency graph sebagai gambar
tuist graph

# Output: graph.png di working directory
# Bisa juga output sebagai JSON untuk analisis programmatic
tuist graph --format json > graph.json

# Focus pada subset graph
tuist graph --targets FeatureCheckout,NetworkKit,SharedUI

Memahami Output Graph

swift
graph.png menunjukkan:
App
├── FeatureHome ──→ NetworkKit
│                └→ SharedUI
├── FeatureCheckout ──→ NetworkKit
│                   ├→ SharedUI
│                   └→ StripeKit (external)
└── FeatureProfile ──→ NetworkKit
                    └→ SharedUI

Dari graph ini bisa analisis:
- NetworkKit dan SharedUI adalah critical path (semua feature depend ke sana)
- StripeKit hanya digunakan FeatureCheckout (isolated dependency)
- Tidak ada circular dependency (graph adalah DAG)

Implicit Import Detection

swift
# Deteksi module yang di-import dalam source tapi tidak di-declare di Project.swift
tuist inspect implicit-imports

# Contoh output:
# ⚠️  FeatureHome imports 'AnalyticsKit' but it's not declared as a dependency
# ⚠️  FeatureCheckout imports 'Stripe' directly, use .external(name: "Stripe")

# Ini adalah source bug yang umum:
# Developer import module yang kebetulan available karena transitive dependency
# Ketika dependency graph berubah, build tiba-tiba gagal

Architecture Linting via Custom Script

swift
// scripts/lint-architecture.swift
// Jalankan: swift scripts/lint-architecture.swift

import Foundation

// Parse graph.json yang di-generate tuist graph --format json
let graphData = try Data(contentsOf: URL(fileURLWithPath: "graph.json"))
let graph = try JSONDecoder().decode(ModuleGraph.self, from: graphData)

// Rules:
// 1. Core modules tidak boleh depend ke Feature modules
// 2. Shared modules tidak boleh depend ke Feature modules
// 3. Circular dependencies tidak boleh ada

var violations: [String] = []

for (module, deps) in graph.dependencies {
    let layer = detectLayer(module)

    for dep in deps {
        let depLayer = detectLayer(dep)

        if layer == .core && depLayer == .feature {
            violations.append("❌ VIOLATION: \(module) (core) depends on \(dep) (feature)")
        }
        if layer == .shared && depLayer == .feature {
            violations.append("❌ VIOLATION: \(module) (shared) depends on \(dep) (feature)")
        }
    }
}

if violations.isEmpty {
    print("✅ Architecture rules passed")
} else {
    violations.forEach { print($0) }
    exit(1)
}

func detectLayer(_ name: String) -> Layer {
    if name.hasPrefix("Feature") { return .feature }
    if name.hasSuffix("Kit") || name.hasSuffix("Service") { return .core }
    if name.hasPrefix("Shared") || name.hasPrefix("Design") { return .shared }
    return .unknown
}

enum Layer { case core, shared, feature, app, unknown }
struct ModuleGraph: Decodable { var dependencies: [String: [String]] }