From 163a3ab94b983307151b447f4c5070b5aee64a13 Mon Sep 17 00:00:00 2001 From: Hutorov Yevhenii Date: Tue, 31 Mar 2026 13:26:43 +0300 Subject: [PATCH] Update image parser --- .../Endpoint/ComponentsEndpoint.swift | 8 +++ Sources/FigmaAPI/Model/FigmaClientError.swift | 2 +- .../FigmaExport/Loaders/ImagesLoader.swift | 67 ++++++++++++++++--- 3 files changed, 66 insertions(+), 11 deletions(-) diff --git a/Sources/FigmaAPI/Endpoint/ComponentsEndpoint.swift b/Sources/FigmaAPI/Endpoint/ComponentsEndpoint.swift index 9865a837..3ab66bdd 100644 --- a/Sources/FigmaAPI/Endpoint/ComponentsEndpoint.swift +++ b/Sources/FigmaAPI/Endpoint/ComponentsEndpoint.swift @@ -42,6 +42,14 @@ public struct Component: Codable { public let name: String public let description: String? public let containingFrame: ContainingFrame + + public init(key: String, nodeId: String, name: String, description: String?, containingFrame: ContainingFrame) { + self.key = key + self.nodeId = nodeId + self.name = name + self.description = description + self.containingFrame = containingFrame + } } // MARK: - ContainingFrame diff --git a/Sources/FigmaAPI/Model/FigmaClientError.swift b/Sources/FigmaAPI/Model/FigmaClientError.swift index f39eccdb..ab7d12c9 100644 --- a/Sources/FigmaAPI/Model/FigmaClientError.swift +++ b/Sources/FigmaAPI/Model/FigmaClientError.swift @@ -7,7 +7,7 @@ struct FigmaClientError: Decodable, LocalizedError { var errorDescription: String? { switch err { case "Not found": - return "Figma file not found. Check lightFileId and darkFileId (if you project supports dark mode) in the yaml config file." + return "Figma file not found. Check lightFileId and darkFileId (if your project supports dark mode) in the yaml config file. Also verify that your personal access token is valid and hasn't expired." default: return "Figma API: \(err)" } diff --git a/Sources/FigmaExport/Loaders/ImagesLoader.swift b/Sources/FigmaExport/Loaders/ImagesLoader.swift index 4b6d963f..8495ec53 100644 --- a/Sources/FigmaExport/Loaders/ImagesLoader.swift +++ b/Sources/FigmaExport/Loaders/ImagesLoader.swift @@ -202,19 +202,62 @@ final class ImagesLoader { // MARK: - Helpers private func fetchImageComponents(fileId: String, frameName: String, filter: String? = nil) throws -> [NodeId: Component] { - var components = try loadComponents(fileId: fileId) - .filter { - $0.containingFrame.name == frameName && $0.useForPlatform(platform) - } + let allComponents = try loadComponents(fileId: fileId) + + // 1. Initial filter based on your Frame Regex and Page name + let filtered = allComponents.filter { component in + guard let name = component.containingFrame.name else { return false } + let isFrameMatch = name.range(of: frameName, options: [.regularExpression, .caseInsensitive]) != nil + let isPageMatch = component.containingFrame.pageName.contains("Icons") + return isFrameMatch && isPageMatch + } - if let filter { - let assetsFilter = AssetsFilter(filter: filter) - components = components.filter { component -> Bool in - assetsFilter.match(name: component.name) + // 2. Prepare for deduplication using the final name as the key + var finalComponentsByName: [String: Component] = [:] + + for component in filtered { + guard let frameName = component.containingFrame.name, frameName.contains(" / ") else { continue } + + let frameBaseName = frameName.components(separatedBy: " / ").last? + .trimmingCharacters(in: .whitespaces) ?? "" + + let variantValue = component.name.components(separatedBy: "=").last? + .trimmingCharacters(in: .whitespaces) ?? "" + + let finalName = (frameBaseName + variantValue.capitalized) + .replacingOccurrences(of: " ", with: "") + + let newComponent = Component( + key: component.key, + nodeId: component.nodeId, + name: finalName, + description: component.description, + containingFrame: component.containingFrame + ) + + // 3. Deduplication: Only store one version for each finalName + if let existing = finalComponentsByName[finalName] { + // Priority: Keep the iOS version if we find multiple + if component.name.lowercased().contains("platform=ios") { + finalComponentsByName[finalName] = newComponent + } + } else { + finalComponentsByName[finalName] = newComponent } } - return Dictionary(uniqueKeysWithValues: components.map { ($0.nodeId, $0) }) + guard !finalComponentsByName.isEmpty else { + throw FigmaExportError.componentsNotFound + } + + // 4. IMPORTANT: Map back using the ORIGINAL nodeId as the key + // This ensures the API call 'GET /v1/images/' uses valid IDs + var result: [NodeId: Component] = [:] + for (_, component) in finalComponentsByName { + result[component.nodeId] = component + } + + return result } private func _loadImages( @@ -266,7 +309,11 @@ final class ImagesLoader { let isRTL = component.useRTL() return Image(name: name, scale: .all, idiom: idiom, url: url, format: params.format, isRTL: isRTL) } - return ImagePack(name: packName, images: packImages, platform: platform) + var imagePack = ImagePack(name: packName, images: packImages, platform: platform) + if let containingFrameName = components.first?.value.containingFrame.name, containingFrameName.hasPrefix("ic / img_") { + imagePack.renderMode = .original + } + return imagePack } return imagePacks }