Skip to content

Commit 1fa768b

Browse files
committed
fix: streamline async file ID extraction and warn on conversion failures
1 parent 1b16ee3 commit 1fa768b

12 files changed

Lines changed: 100 additions & 272 deletions

‎Package.swift‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -214,13 +214,6 @@ let package = Package(
214214
.product(name: "CustomDump", package: "swift-custom-dump"),
215215
]
216216
),
217-
.testTarget(
218-
name: "ExFigConfigTests",
219-
dependencies: [
220-
"ExFigConfig",
221-
.product(name: "CustomDump", package: "swift-custom-dump"),
222-
]
223-
),
224217
.testTarget(
225218
name: "XcodeExportTests",
226219
dependencies: [

‎Sources/ExFig-Android/Config/AndroidTypographyEntry.swift‎

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,8 @@ public extension Android.Typography {
1717
)
1818
}
1919

20-
/// Converts PKL NameStyle to ExFigCore NameStyle.
20+
/// Converts PKL NameStyle to ExFigCore NameStyle via centralized bridging.
2121
var coreNameStyle: NameStyle {
22-
switch nameStyle {
23-
case .camelCase: .camelCase
24-
case .snake_case: .snakeCase
25-
case .pascalCase: .pascalCase
26-
case .flatCase: .flatCase
27-
case .kebabCase: .kebabCase
28-
case .sCREAMING_SNAKE_CASE: .screamingSnakeCase
29-
}
22+
nameStyle.coreNameStyle
3023
}
3124
}

‎Sources/ExFig-Android/Export/AndroidIconsExporter.swift‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,15 +208,20 @@ private extension AndroidIconsExporter {
208208

209209
// Collect SVG data
210210
var svgFiles: [String: Data] = [:]
211+
var failedCount = 0
211212
for file in localFiles {
212213
let iconName = file.destination.file.deletingPathExtension().lastPathComponent
213214
do {
214215
let data = try Data(contentsOf: file.destination.url)
215216
svgFiles[iconName] = data
216217
} catch {
218+
failedCount += 1
217219
context.warning("Failed to read SVG file '\(iconName)': \(error.localizedDescription)")
218220
}
219221
}
222+
if failedCount > 0 {
223+
context.warning("\(failedCount) icon(s) skipped due to read failures")
224+
}
220225

221226
if context.filter == nil {
222227
try? FileManager.default.removeItem(atPath: outputDirectory.path)

‎Sources/ExFig-Android/Export/AndroidImagesExporter.swift‎

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -155,20 +155,26 @@ private extension AndroidImagesExporter {
155155
)
156156

157157
let isSingleScale = scales.count == 1
158-
let finalFiles = webpFiles.compactMap { file -> FileContents? in
159-
guard let data = file.data else { return nil }
158+
var collectedFiles: [FileContents] = []
159+
for file in webpFiles {
160+
guard let data = file.data else {
161+
let name = file.destination.file.lastPathComponent
162+
context.warning("Skipped image '\(name)': no data after WebP conversion")
163+
continue
164+
}
160165
let stripped = file.strippingScaleSuffix()
161166
let dirName = Drawable.scaleToDrawableName(file.scale, dark: file.dark, singleScale: isSingleScale)
162167
let directory = resolvedMainRes
163168
.appendingPathComponent(entry.output)
164169
.appendingPathComponent(dirName, isDirectory: true)
165-
return FileContents(
170+
collectedFiles.append(FileContents(
166171
destination: Destination(directory: directory, file: stripped.destination.file),
167172
data: data,
168173
scale: file.scale,
169174
dark: file.dark
170-
)
175+
))
171176
}
177+
let finalFiles = collectedFiles
172178

173179
try await context.withSpinner("Writing files to Android project...") {
174180
try context.writeFiles(finalFiles)
@@ -208,20 +214,26 @@ private extension AndroidImagesExporter {
208214
)
209215

210216
let isSingleScale = scales.count == 1
211-
let finalFiles = pngFiles.compactMap { file -> FileContents? in
212-
guard let data = file.data else { return nil }
217+
var collectedFiles: [FileContents] = []
218+
for file in pngFiles {
219+
guard let data = file.data else {
220+
let name = file.destination.file.lastPathComponent
221+
context.warning("Skipped image '\(name)': no data after PNG conversion")
222+
continue
223+
}
213224
let stripped = file.strippingScaleSuffix()
214225
let dirName = Drawable.scaleToDrawableName(file.scale, dark: file.dark, singleScale: isSingleScale)
215226
let directory = resolvedMainRes
216227
.appendingPathComponent(entry.output)
217228
.appendingPathComponent(dirName, isDirectory: true)
218-
return FileContents(
229+
collectedFiles.append(FileContents(
219230
destination: Destination(directory: directory, file: stripped.destination.file),
220231
data: data,
221232
scale: file.scale,
222233
dark: file.dark
223-
)
234+
))
224235
}
236+
let finalFiles = collectedFiles
225237

226238
try await context.withSpinner("Writing files to Android project...") {
227239
try context.writeFiles(finalFiles)

‎Sources/ExFig-Flutter/Export/FlutterImagesExporter.swift‎

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@ import ExFigCore
44
import FlutterExport
55
import Foundation
66

7+
/// Errors thrown during Flutter image export.
8+
enum FlutterImagesExportError: LocalizedError {
9+
case incompatibleFormat(source: String, output: String)
10+
11+
var errorDescription: String? {
12+
switch self {
13+
case let .incompatibleFormat(source, output):
14+
"Incompatible format: cannot convert \(source) source to \(output)"
15+
}
16+
}
17+
18+
var recoverySuggestion: String? {
19+
"Use SVG source format for vector output, or PNG/WebP for raster output."
20+
}
21+
}
22+
723
/// Exports images from Figma frames to Flutter assets and Dart code.
824
///
925
/// Supports multiple workflows:
@@ -70,8 +86,7 @@ public struct FlutterImagesExporter: ImagesExporter {
7086
entry: entry, platformConfig: platformConfig, context: context
7187
)
7288
case (.png, .svg):
73-
context.warning("Cannot convert PNG source to SVG. Use SVG source for vector output.")
74-
return 0
89+
throw FlutterImagesExportError.incompatibleFormat(source: "PNG", output: "SVG")
7590
}
7691
}
7792
}
@@ -145,7 +160,8 @@ private extension FlutterImagesExporter {
145160
}
146161

147162
let finalFiles = FlutterImagesHelpers.mapToFlutterScaleDirectories(
148-
webpFiles, assetsDirectory: assetsDirectory
163+
webpFiles, assetsDirectory: assetsDirectory,
164+
onSkip: { context.warning("Skipped image '\($0)': no data after conversion") }
149165
)
150166

151167
let resolvedTemplatesPath = entry.resolvedTemplatesPath(fallback: platformConfig.templatesPath)
@@ -197,7 +213,8 @@ private extension FlutterImagesExporter {
197213
}
198214

199215
let finalFiles = FlutterImagesHelpers.mapToFlutterScaleDirectories(
200-
pngFiles, assetsDirectory: assetsDirectory
216+
pngFiles, assetsDirectory: assetsDirectory,
217+
onSkip: { context.warning("Skipped image '\($0)': no data after conversion") }
201218
)
202219

203220
let resolvedTemplatesPath = entry.resolvedTemplatesPath(fallback: platformConfig.templatesPath)
@@ -249,7 +266,8 @@ private extension FlutterImagesExporter {
249266
}
250267

251268
let finalFiles = FlutterImagesHelpers.mapToFlutterScaleDirectories(
252-
localFiles, assetsDirectory: assetsDirectory
269+
localFiles, assetsDirectory: assetsDirectory,
270+
onSkip: { context.warning("Skipped image '\($0)': no data after conversion") }
253271
)
254272

255273
let resolvedTemplatesPath = entry.resolvedTemplatesPath(fallback: platformConfig.templatesPath)
@@ -533,27 +551,31 @@ enum FlutterImagesHelpers {
533551

534552
static func mapToFlutterScaleDirectories(
535553
_ files: [FileContents],
536-
assetsDirectory: URL
554+
assetsDirectory: URL,
555+
onSkip: ((String) -> Void)? = nil
537556
) -> [FileContents] {
538-
files.compactMap { file -> FileContents? in
557+
var result: [FileContents] = []
558+
for file in files {
539559
let scaleDirectory = file.scale == 1.0
540560
? assetsDirectory
541561
: assetsDirectory.appendingPathComponent("\(file.scale)x")
542562
let cleanFile = file.strippingScaleSuffix().destination.file
543563

544564
if let data = file.data {
545-
return FileContents(
565+
result.append(FileContents(
546566
destination: Destination(directory: scaleDirectory, file: cleanFile),
547567
data: data, scale: file.scale, dark: file.dark
548-
)
568+
))
549569
} else if let dataFile = file.dataFile {
550-
return FileContents(
570+
result.append(FileContents(
551571
destination: Destination(directory: scaleDirectory, file: cleanFile),
552572
dataFile: dataFile, scale: file.scale, dark: file.dark
553-
)
573+
))
574+
} else {
575+
onSkip?(cleanFile.lastPathComponent)
554576
}
555-
return nil
556577
}
578+
return result
557579
}
558580
}
559581

‎Sources/ExFigCLI/Batch/FileIdExtractor.swift‎

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,34 +20,15 @@ struct FileIdExtractor {
2020
///
2121
/// - Parameter configURLs: URLs to config files.
2222
/// - Returns: Set of unique file IDs found across all configs.
23-
func extractUniqueFileIds(from configURLs: [URL]) -> Set<String> {
23+
func extractUniqueFileIds(from configURLs: [URL]) async -> Set<String> {
2424
var fileIds = Set<String>()
2525

2626
for configURL in configURLs {
27-
if let params = parseParams(from: configURL) {
28-
fileIds.formUnion(params.getFileIds())
29-
}
30-
}
31-
32-
return fileIds
33-
}
34-
35-
/// Parse ExFig.ModuleImpl from a PKL config file URL.
36-
///
37-
/// - Parameter url: URL to the config file.
38-
/// - Returns: Parsed ExFig.ModuleImpl or nil if parsing fails.
39-
private func parseParams(from url: URL) -> ExFig.ModuleImpl? {
40-
// Run async evaluation synchronously
41-
// Semaphore ensures sequential access, so @unchecked Sendable is safe
42-
let semaphore = DispatchSemaphore(value: 0)
43-
let box = SendableBox<ExFig.ModuleImpl?>(nil)
44-
45-
Task {
46-
defer { semaphore.signal() }
4727
do {
48-
box.value = try await PKLEvaluator.evaluate(configPath: url)
28+
let module: ExFig.ModuleImpl = try await PKLEvaluator.evaluate(configPath: configURL)
29+
fileIds.formUnion(module.getFileIds())
4930
} catch {
50-
let name = url.lastPathComponent
31+
let name = configURL.lastPathComponent
5132
let reason = error.localizedDescription
5233
ExFigCommand.logger.error(
5334
"""
@@ -58,14 +39,6 @@ struct FileIdExtractor {
5839
}
5940
}
6041

61-
let waitResult = semaphore.wait(timeout: .now() + 30)
62-
guard waitResult == .success else {
63-
let name = url.lastPathComponent
64-
ExFigCommand.logger.error(
65-
"Pre-fetch optimization: PKL evaluation timed out for \(name)."
66-
)
67-
return nil
68-
}
69-
return box.value
42+
return fileIds
7043
}
7144
}

‎Sources/ExFigCLI/Batch/FileVersionPreFetcher.swift‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ struct FileVersionPreFetcher: Sendable {
7676
// Extract unique file IDs from all configs
7777
let extractor = FileIdExtractor()
7878
let configURLs = configuration.configs.map(\.url)
79-
let uniqueFileIds = extractor.extractUniqueFileIds(from: configURLs)
79+
let uniqueFileIds = await extractor.extractUniqueFileIds(from: configURLs)
8080

8181
guard !uniqueFileIds.isEmpty else {
8282
return nil
@@ -135,7 +135,7 @@ struct FileVersionPreFetcher: Sendable {
135135
}
136136

137137
// Extract unique file IDs from all configs
138-
let uniqueFileIds = extractUniqueFileIds(from: configuration)
138+
let uniqueFileIds = await extractUniqueFileIds(from: configuration)
139139
guard !uniqueFileIds.isEmpty else {
140140
return PreFetchResult(versions: nil, components: nil, nodes: nil)
141141
}
@@ -164,10 +164,10 @@ struct FileVersionPreFetcher: Sendable {
164164
}
165165

166166
/// Extracts unique file IDs from configuration.
167-
private static func extractUniqueFileIds(from configuration: PreFetchConfiguration) -> Set<String> {
167+
private static func extractUniqueFileIds(from configuration: PreFetchConfiguration) async -> Set<String> {
168168
let extractor = FileIdExtractor()
169169
let configURLs = configuration.configs.map(\.url)
170-
return extractor.extractUniqueFileIds(from: configURLs)
170+
return await extractor.extractUniqueFileIds(from: configURLs)
171171
}
172172

173173
/// Creates a rate-limited pre-fetcher.

‎Sources/ExFigConfig/NameProcessingConfig.swift‎

Lines changed: 0 additions & 32 deletions
This file was deleted.

‎Sources/ExFigConfig/SourceConfig.swift‎

Lines changed: 0 additions & 46 deletions
This file was deleted.

0 commit comments

Comments
 (0)