1+ // swiftlint:disable file_length
12import ExFigConfig
23import FigmaAPI
34import Foundation
@@ -37,6 +38,7 @@ struct SubcommandConfigExporter: ConfigExportPerforming {
3738 try await runExports ( options: options, client: client, ui: ui)
3839 }
3940
41+ // swiftlint:disable function_body_length cyclomatic_complexity
4042 private func runExports(
4143 options: ExFigOptions ,
4244 client: Client ,
@@ -47,65 +49,115 @@ struct SubcommandConfigExporter: ConfigExportPerforming {
4749 let heavyFaultTolerance = makeHeavyFaultToleranceOptions ( )
4850 let params = options. params
4951
52+ enum StepResult : Sendable {
53+ case colors( ExportStats )
54+ case icons( ExportStats )
55+ case images( ExportStats )
56+ case typography( ExportStats )
57+ }
58+
5059 var stats = ExportStats . zero
5160
52- // Colors export
53- if hasColorsConfig ( params) {
54- let cmd = makeColors ( options: options, cacheOptions: cacheOpts, faultToleranceOptions: faultTolerance)
55- let result = try await cmd. performExportWithResult (
56- client: client,
57- ui: ui,
58- context: configContext. with ( assetType: . colors)
59- )
60- stats += ExportStats ( colors: result. count, fileVersions: result. fileVersions)
61- }
61+ try await withThrowingTaskGroup ( of: StepResult . self) { [ self ] group in
62+ if hasColorsConfig ( params) {
63+ group. addTask { [ self ] in
64+ let cmd = makeColors (
65+ options: options,
66+ cacheOptions: cacheOpts,
67+ faultToleranceOptions: faultTolerance
68+ )
69+ let result = try await cmd. performExportWithResult (
70+ client: client,
71+ ui: ui,
72+ context: configContext. with ( assetType: . colors)
73+ )
74+ return . colors( ExportStats ( colors: result. count, fileVersions: result. fileVersions) )
75+ }
76+ }
6277
63- // Icons export
64- if hasIconsConfig ( params) {
65- let cmd = makeIcons ( options: options, cacheOptions: cacheOpts, faultToleranceOptions: heavyFaultTolerance)
66- let result = try await cmd. performExportWithResult (
67- client: client,
68- ui: ui,
69- context: configContext. with ( assetType: . icons)
70- )
71- stats += ExportStats (
72- icons: result. count,
73- computedNodeHashes: result. computedHashes,
74- granularCacheStats: result. granularCacheStats,
75- fileVersions: result. fileVersions
76- )
77- }
78+ if hasIconsConfig ( params) {
79+ group. addTask { [ self ] in
80+ let cmd = makeIcons (
81+ options: options, cacheOptions: cacheOpts, faultToleranceOptions: heavyFaultTolerance
82+ )
83+ let result = try await cmd. performExportWithResult (
84+ client: client,
85+ ui: ui,
86+ context: configContext. with ( assetType: . icons)
87+ )
88+ return . icons( ExportStats (
89+ icons: result. count,
90+ computedNodeHashes: result. computedHashes,
91+ granularCacheStats: result. granularCacheStats,
92+ fileVersions: result. fileVersions
93+ ) )
94+ }
95+ }
7896
79- // Images export
80- if hasImagesConfig ( params) {
81- let cmd = makeImages ( options: options, cacheOptions: cacheOpts, faultToleranceOptions: heavyFaultTolerance)
82- let result = try await cmd. performExportWithResult (
83- client: client,
84- ui: ui,
85- context: configContext. with ( assetType: . images)
86- )
87- stats += ExportStats (
88- images: result. count,
89- computedNodeHashes: result. computedHashes,
90- granularCacheStats: result. granularCacheStats,
91- fileVersions: result. fileVersions
92- )
93- }
97+ if hasImagesConfig ( params) {
98+ group. addTask { [ self ] in
99+ let cmd = makeImages (
100+ options: options, cacheOptions: cacheOpts, faultToleranceOptions: heavyFaultTolerance
101+ )
102+ let result = try await cmd. performExportWithResult (
103+ client: client,
104+ ui: ui,
105+ context: configContext. with ( assetType: . images)
106+ )
107+ return . images( ExportStats (
108+ images: result. count,
109+ computedNodeHashes: result. computedHashes,
110+ granularCacheStats: result. granularCacheStats,
111+ fileVersions: result. fileVersions
112+ ) )
113+ }
114+ }
94115
95- // Typography export
96- if hasTypographyConfig ( params) {
97- let cmd = makeTypography ( options: options, cacheOptions: cacheOpts, faultToleranceOptions: faultTolerance)
98- let result = try await cmd. performExportWithResult (
99- client: client,
100- ui: ui,
101- context: configContext. with ( assetType: . typography)
102- )
103- stats += ExportStats ( typography: result. count, fileVersions: result. fileVersions)
116+ if hasTypographyConfig ( params) {
117+ group. addTask { [ self ] in
118+ let cmd = makeTypography (
119+ options: options, cacheOptions: cacheOpts, faultToleranceOptions: faultTolerance
120+ )
121+ let result = try await cmd. performExportWithResult (
122+ client: client,
123+ ui: ui,
124+ context: configContext. with ( assetType: . typography)
125+ )
126+ return . typography( ExportStats ( typography: result. count, fileVersions: result. fileVersions) )
127+ }
128+ }
129+
130+ for try await stepResult in group {
131+ switch stepResult {
132+ case let . colors( s) :
133+ stats += s
134+ if let cb = configContext. stepCompletionCallback {
135+ await cb ( . colors, s. colors)
136+ }
137+ case let . icons( s) :
138+ stats += s
139+ if let cb = configContext. stepCompletionCallback {
140+ await cb ( . icons, s. icons)
141+ }
142+ case let . images( s) :
143+ stats += s
144+ if let cb = configContext. stepCompletionCallback {
145+ await cb ( . images, s. images)
146+ }
147+ case let . typography( s) :
148+ stats += s
149+ if let cb = configContext. stepCompletionCallback {
150+ await cb ( . typography, s. typography)
151+ }
152+ }
153+ }
104154 }
105155
106156 return stats
107157 }
108158
159+ // swiftlint:enable function_body_length cyclomatic_complexity
160+
109161 private func makeCacheOptions( ) -> CacheOptions {
110162 var options = CacheOptions ( )
111163 options. cache = cache
@@ -209,6 +261,26 @@ struct BatchConfigRunner: Sendable {
209261 _testExporter = exporter
210262 }
211263
264+ // swiftlint:disable cyclomatic_complexity
265+
266+ /// Count the number of active asset types in a config (colors, icons, images, typography).
267+ static func countActiveAssetTypes( _ params: ExFig . ModuleImpl ? ) -> Int {
268+ guard let params else { return 0 }
269+ let checks : [ Bool ] = [
270+ params. common? . colors != nil || params. common? . variablesColors != nil
271+ || params. ios? . colors != nil || params. android? . colors != nil
272+ || params. flutter? . colors != nil || params. web? . colors != nil ,
273+ params. ios? . icons != nil || params. android? . icons != nil
274+ || params. flutter? . icons != nil || params. web? . icons != nil ,
275+ params. ios? . images != nil || params. android? . images != nil
276+ || params. flutter? . images != nil || params. web? . images != nil ,
277+ params. ios? . typography != nil || params. android? . typography != nil ,
278+ ]
279+ return checks. filter { $0 } . count
280+ }
281+
282+ // swiftlint:enable cyclomatic_complexity
283+
212284 // swiftlint:disable:next function_body_length
213285 func process(
214286 configFile: ConfigFile ,
@@ -268,11 +340,28 @@ struct BatchConfigRunner: Sendable {
268340 nil
269341 }
270342
343+ // Create step completion callback that routes to batch progress view
344+ let stepCallback : ConfigExecutionContext . StepCompletionCallback ? =
345+ if let pv = progressView {
346+ { ( assetType: ConfigExecutionContext . AssetType , count: Int ) in
347+ await pv. completeExportStep ( name: configName, assetType: assetType, count: count)
348+ }
349+ } else {
350+ nil
351+ }
352+
353+ // Set total steps before export starts
354+ if let pv = progressView {
355+ let totalSteps = Self . countActiveAssetTypes ( options. params)
356+ await pv. setTotalSteps ( name: configName, total: totalSteps)
357+ }
358+
271359 // Create per-config execution context (passed explicitly, no TaskLocal nesting)
272360 let configContext = ConfigExecutionContext (
273361 configId: configFile. name,
274362 configPriority: configPriority,
275- downloadProgressCallback: downloadCallback
363+ downloadProgressCallback: downloadCallback,
364+ stepCompletionCallback: stepCallback
276365 )
277366
278367 // Use test exporter if provided, otherwise create real exporter
@@ -296,15 +385,8 @@ struct BatchConfigRunner: Sendable {
296385 ui: ui
297386 )
298387
299- // Update progress view with final counts or log success
388+ // Mark config as succeeded (step progress already updated incrementally)
300389 if let progressView {
301- await progressView. updateProgress (
302- name: configFile. name,
303- colors: stats. colors > 0 ? ( stats. colors, stats. colors) : nil ,
304- icons: stats. icons > 0 ? ( stats. icons, stats. icons) : nil ,
305- images: stats. images > 0 ? ( stats. images, stats. images) : nil ,
306- typography: stats. typography > 0 ? ( stats. typography, stats. typography) : nil
307- )
308390 await progressView. succeedConfig ( name: configFile. name)
309391 } else {
310392 ui. success ( " Completed: \( configFile. name) " )
0 commit comments