|
| 1 | +import ExFigCore |
| 2 | +import Foundation |
| 3 | +import Stencil |
| 4 | + |
| 5 | +/// Generates Figma Code Connect Kotlin files for Jetpack Compose. |
| 6 | +/// |
| 7 | +/// Code Connect files link Figma design components to Compose code, |
| 8 | +/// enabling designers to see the corresponding Compose implementation |
| 9 | +/// in Figma Dev Mode. |
| 10 | +public final class AndroidCodeConnectExporter: AndroidExporter { |
| 11 | + override public init(templatesPath: URL? = nil) { |
| 12 | + super.init(templatesPath: templatesPath) |
| 13 | + } |
| 14 | + |
| 15 | + /// Generates a Code Connect Kotlin file from asset packs (icons or images). |
| 16 | + /// |
| 17 | + /// - Parameters: |
| 18 | + /// - imagePacks: Asset packs with nodeId and fileId for Code Connect URLs. |
| 19 | + /// - url: Output URL for the generated `.figma.kt` file. |
| 20 | + /// - packageName: Kotlin package name for the generated file. |
| 21 | + /// - xmlResourcePackage: Package for the `R` class import. |
| 22 | + /// - allAssetMetadata: Optional full asset metadata for granular cache mode. |
| 23 | + /// When provided, generates Code Connect for ALL assets (not just changed ones). |
| 24 | + /// - Returns: File contents to write, or nil if no valid assets with nodeId. |
| 25 | + public func generateCodeConnect( |
| 26 | + imagePacks: [AssetPair<ImagePack>], |
| 27 | + url: URL, |
| 28 | + packageName: String, |
| 29 | + xmlResourcePackage: String, |
| 30 | + allAssetMetadata: [AssetMetadata]? = nil |
| 31 | + ) throws -> FileContents? { |
| 32 | + let assets: [[String: String]] |
| 33 | + |
| 34 | + if let allMetadata = allAssetMetadata, !allMetadata.isEmpty { |
| 35 | + let validMetadata = allMetadata.filter { !$0.nodeId.isEmpty && !$0.fileId.isEmpty } |
| 36 | + guard !validMetadata.isEmpty else { return nil } |
| 37 | + assets = validMetadata.map { meta in |
| 38 | + makeAssetContext(name: meta.name, nodeId: meta.nodeId, fileId: meta.fileId) |
| 39 | + } |
| 40 | + } else { |
| 41 | + assets = imagePacks.compactMap { pack -> [String: String]? in |
| 42 | + guard let nodeId = pack.light.nodeId, let fileId = pack.light.fileId else { return nil } |
| 43 | + return makeAssetContext(name: pack.light.name, nodeId: nodeId, fileId: fileId) |
| 44 | + } |
| 45 | + guard !assets.isEmpty else { return nil } |
| 46 | + } |
| 47 | + |
| 48 | + let sortedAssets = assets.sorted { ($0["name"] ?? "") < ($1["name"] ?? "") } |
| 49 | + |
| 50 | + let context: [String: Any] = [ |
| 51 | + "package": packageName, |
| 52 | + "xmlResourcePackage": xmlResourcePackage, |
| 53 | + "assets": sortedAssets, |
| 54 | + ] |
| 55 | + |
| 56 | + let env = makeEnvironment() |
| 57 | + let contents = try env.renderTemplate(name: "CodeConnect.figma.kt.stencil", context: context) |
| 58 | + |
| 59 | + let directory = url.deletingLastPathComponent() |
| 60 | + let file = URL(fileURLWithPath: url.lastPathComponent) |
| 61 | + return try makeFileContents(for: contents, directory: directory, file: file) |
| 62 | + } |
| 63 | + |
| 64 | + // MARK: - Private |
| 65 | + |
| 66 | + /// Builds a template context dictionary for a single asset. |
| 67 | + /// |
| 68 | + /// - `resourceName`: sanitized for `R.drawable.*` (non-alphanumeric → `_`, no leading digit). |
| 69 | + /// - `className`: unique Composable function name (`Asset_<sanitized>`). |
| 70 | + private func makeAssetContext(name: String, nodeId: String, fileId: String) -> [String: String] { |
| 71 | + let urlNodeId = nodeId.replacingOccurrences(of: ":", with: "-") |
| 72 | + let sanitizedName = name.map { $0.isLetter || $0.isNumber ? $0 : Character("_") } |
| 73 | + let className = "Asset_\(String(sanitizedName))" |
| 74 | + |
| 75 | + // Sanitize for R.drawable: only [a-z0-9_], no leading digit |
| 76 | + var resourceName = name.map { $0.isLetter || $0.isNumber || $0 == Character("_") ? $0 : Character("_") } |
| 77 | + if let first = resourceName.first, first.isNumber { |
| 78 | + resourceName.insert(Character("_"), at: resourceName.startIndex) |
| 79 | + } |
| 80 | + |
| 81 | + let figmaUrl = "https://www.figma.com/design/\(fileId)?node-id=\(urlNodeId)" |
| 82 | + |
| 83 | + return [ |
| 84 | + "name": name, |
| 85 | + "resourceName": String(resourceName), |
| 86 | + "className": className, |
| 87 | + "nodeId": urlNodeId, |
| 88 | + "fileId": fileId, |
| 89 | + "figmaUrl": figmaUrl, |
| 90 | + ] |
| 91 | + } |
| 92 | +} |
0 commit comments