objcjs-types 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/bin/objcjs-types.ts +31 -0
  2. package/dist/AVFoundation/functions.d.ts +21 -0
  3. package/dist/AVFoundation/functions.js +32 -0
  4. package/dist/AVFoundation/index.d.ts +1 -0
  5. package/dist/AVFoundation/index.js +1 -0
  6. package/dist/Accessibility/functions.d.ts +16 -0
  7. package/dist/Accessibility/functions.js +35 -0
  8. package/dist/Accessibility/index.d.ts +1 -0
  9. package/dist/Accessibility/index.js +1 -0
  10. package/dist/AddressBook/functions.d.ts +98 -0
  11. package/dist/AddressBook/functions.js +290 -0
  12. package/dist/AddressBook/index.d.ts +1 -0
  13. package/dist/AddressBook/index.js +1 -0
  14. package/dist/AppKit/functions.d.ts +112 -0
  15. package/dist/AppKit/functions.js +272 -0
  16. package/dist/AppKit/index.d.ts +1 -0
  17. package/dist/AppKit/index.js +1 -0
  18. package/dist/AudioToolbox/functions.d.ts +377 -0
  19. package/dist/AudioToolbox/functions.js +1124 -0
  20. package/dist/AudioToolbox/index.d.ts +1 -0
  21. package/dist/AudioToolbox/index.js +1 -0
  22. package/dist/AuthenticationServices/functions.d.ts +2 -0
  23. package/dist/AuthenticationServices/functions.js +5 -0
  24. package/dist/AuthenticationServices/index.d.ts +1 -0
  25. package/dist/AuthenticationServices/index.js +1 -0
  26. package/dist/BrowserEngineCore/functions.d.ts +3 -0
  27. package/dist/BrowserEngineCore/functions.js +11 -0
  28. package/dist/BrowserEngineCore/index.d.ts +1 -0
  29. package/dist/BrowserEngineCore/index.js +1 -0
  30. package/dist/CoreAudio/functions.d.ts +60 -0
  31. package/dist/CoreAudio/functions.js +173 -0
  32. package/dist/CoreAudio/index.d.ts +1 -0
  33. package/dist/CoreAudio/index.js +1 -0
  34. package/dist/CoreMIDI/functions.d.ts +96 -0
  35. package/dist/CoreMIDI/functions.js +287 -0
  36. package/dist/CoreMIDI/index.d.ts +1 -0
  37. package/dist/CoreMIDI/index.js +1 -0
  38. package/dist/CoreML/functions.d.ts +2 -0
  39. package/dist/CoreML/functions.js +5 -0
  40. package/dist/CoreML/index.d.ts +1 -0
  41. package/dist/CoreML/index.js +1 -0
  42. package/dist/CoreMediaIO/functions.d.ts +38 -0
  43. package/dist/CoreMediaIO/functions.js +107 -0
  44. package/dist/CoreMediaIO/index.d.ts +1 -0
  45. package/dist/CoreMediaIO/index.js +1 -0
  46. package/dist/CoreText/functions.d.ts +209 -0
  47. package/dist/CoreText/functions.js +611 -0
  48. package/dist/CoreText/index.d.ts +1 -0
  49. package/dist/CoreText/index.js +1 -0
  50. package/dist/CoreWLAN/functions.d.ts +23 -0
  51. package/dist/CoreWLAN/functions.js +56 -0
  52. package/dist/CoreWLAN/index.d.ts +1 -0
  53. package/dist/CoreWLAN/index.js +1 -0
  54. package/dist/DeviceDiscoveryExtension/functions.d.ts +11 -0
  55. package/dist/DeviceDiscoveryExtension/functions.js +17 -0
  56. package/dist/DeviceDiscoveryExtension/index.d.ts +1 -0
  57. package/dist/DeviceDiscoveryExtension/index.js +1 -0
  58. package/dist/DiscRecording/functions.d.ts +97 -0
  59. package/dist/DiscRecording/functions.js +290 -0
  60. package/dist/DiscRecording/index.d.ts +1 -0
  61. package/dist/DiscRecording/index.js +1 -0
  62. package/dist/DiscRecordingUI/functions.d.ts +13 -0
  63. package/dist/DiscRecordingUI/functions.js +38 -0
  64. package/dist/DiscRecordingUI/index.d.ts +1 -0
  65. package/dist/DiscRecordingUI/index.js +1 -0
  66. package/dist/ExceptionHandling/functions.d.ts +1 -0
  67. package/dist/ExceptionHandling/functions.js +5 -0
  68. package/dist/ExceptionHandling/index.d.ts +1 -0
  69. package/dist/ExceptionHandling/index.js +1 -0
  70. package/dist/FSKit/functions.d.ts +4 -0
  71. package/dist/FSKit/functions.js +11 -0
  72. package/dist/FSKit/index.d.ts +1 -0
  73. package/dist/FSKit/index.js +1 -0
  74. package/dist/Foundation/functions.d.ts +145 -0
  75. package/dist/Foundation/functions.js +386 -0
  76. package/dist/Foundation/index.d.ts +1 -0
  77. package/dist/Foundation/index.js +1 -0
  78. package/dist/GLKit/functions.d.ts +51 -0
  79. package/dist/GLKit/functions.js +146 -0
  80. package/dist/GLKit/index.d.ts +1 -0
  81. package/dist/GLKit/index.js +1 -0
  82. package/dist/GameController/functions.d.ts +18 -0
  83. package/dist/GameController/functions.js +44 -0
  84. package/dist/GameController/index.d.ts +1 -0
  85. package/dist/GameController/index.js +1 -0
  86. package/dist/HealthKit/functions.d.ts +19 -0
  87. package/dist/HealthKit/functions.js +35 -0
  88. package/dist/HealthKit/index.d.ts +1 -0
  89. package/dist/HealthKit/index.js +1 -0
  90. package/dist/IOSurface/functions.d.ts +53 -0
  91. package/dist/IOSurface/functions.js +155 -0
  92. package/dist/IOSurface/index.d.ts +1 -0
  93. package/dist/IOSurface/index.js +1 -0
  94. package/dist/IOUSBHost/functions.d.ts +44 -0
  95. package/dist/IOUSBHost/functions.js +131 -0
  96. package/dist/IOUSBHost/index.d.ts +1 -0
  97. package/dist/IOUSBHost/index.js +1 -0
  98. package/dist/InstantMessage/functions.d.ts +1 -0
  99. package/dist/InstantMessage/functions.js +5 -0
  100. package/dist/InstantMessage/index.d.ts +1 -0
  101. package/dist/InstantMessage/index.js +1 -0
  102. package/dist/JavaRuntimeSupport/functions.d.ts +40 -0
  103. package/dist/JavaRuntimeSupport/functions.js +113 -0
  104. package/dist/JavaRuntimeSupport/index.d.ts +1 -0
  105. package/dist/JavaRuntimeSupport/index.js +1 -0
  106. package/dist/JavaScriptCore/functions.d.ts +120 -0
  107. package/dist/JavaScriptCore/functions.js +359 -0
  108. package/dist/JavaScriptCore/index.d.ts +1 -0
  109. package/dist/JavaScriptCore/index.js +1 -0
  110. package/dist/MLCompute/functions.d.ts +27 -0
  111. package/dist/MLCompute/functions.js +41 -0
  112. package/dist/MLCompute/index.d.ts +1 -0
  113. package/dist/MLCompute/index.js +1 -0
  114. package/dist/MapKit/functions.d.ts +23 -0
  115. package/dist/MapKit/functions.js +56 -0
  116. package/dist/MapKit/index.d.ts +1 -0
  117. package/dist/MapKit/index.js +1 -0
  118. package/dist/Matter/functions.d.ts +17 -0
  119. package/dist/Matter/functions.js +26 -0
  120. package/dist/Matter/index.d.ts +1 -0
  121. package/dist/Matter/index.js +1 -0
  122. package/dist/MediaAccessibility/functions.d.ts +28 -0
  123. package/dist/MediaAccessibility/functions.js +83 -0
  124. package/dist/MediaAccessibility/index.d.ts +1 -0
  125. package/dist/MediaAccessibility/index.js +1 -0
  126. package/dist/MediaPlayer/functions.d.ts +3 -0
  127. package/dist/MediaPlayer/functions.js +11 -0
  128. package/dist/MediaPlayer/index.d.ts +1 -0
  129. package/dist/MediaPlayer/index.js +1 -0
  130. package/dist/Metal/functions.d.ts +14 -0
  131. package/dist/Metal/functions.js +26 -0
  132. package/dist/Metal/index.d.ts +1 -0
  133. package/dist/Metal/index.js +1 -0
  134. package/dist/MetalKit/functions.d.ts +11 -0
  135. package/dist/MetalKit/functions.js +20 -0
  136. package/dist/MetalKit/index.d.ts +1 -0
  137. package/dist/MetalKit/index.js +1 -0
  138. package/dist/MetalPerformanceShaders/functions.d.ts +7 -0
  139. package/dist/MetalPerformanceShaders/functions.js +14 -0
  140. package/dist/MetalPerformanceShaders/index.d.ts +1 -0
  141. package/dist/MetalPerformanceShaders/index.js +1 -0
  142. package/dist/NearbyInteraction/functions.d.ts +3 -0
  143. package/dist/NearbyInteraction/functions.js +5 -0
  144. package/dist/NearbyInteraction/index.d.ts +1 -0
  145. package/dist/NearbyInteraction/index.js +1 -0
  146. package/dist/ParavirtualizedGraphics/functions.d.ts +7 -0
  147. package/dist/ParavirtualizedGraphics/functions.js +14 -0
  148. package/dist/ParavirtualizedGraphics/index.d.ts +1 -0
  149. package/dist/ParavirtualizedGraphics/index.js +1 -0
  150. package/dist/QuartzCore/functions.d.ts +19 -0
  151. package/dist/QuartzCore/functions.js +50 -0
  152. package/dist/QuartzCore/index.d.ts +1 -0
  153. package/dist/QuartzCore/index.js +1 -0
  154. package/dist/SceneKit/functions.d.ts +17 -0
  155. package/dist/SceneKit/functions.js +38 -0
  156. package/dist/SceneKit/index.d.ts +1 -0
  157. package/dist/SceneKit/index.js +1 -0
  158. package/dist/SensorKit/functions.d.ts +4 -0
  159. package/dist/SensorKit/functions.js +14 -0
  160. package/dist/SensorKit/index.d.ts +1 -0
  161. package/dist/SensorKit/index.js +1 -0
  162. package/dist/ServiceManagement/functions.d.ts +7 -0
  163. package/dist/ServiceManagement/functions.js +20 -0
  164. package/dist/ServiceManagement/index.d.ts +1 -0
  165. package/dist/ServiceManagement/index.js +1 -0
  166. package/dist/StoreKit/functions.d.ts +1 -0
  167. package/dist/StoreKit/functions.js +5 -0
  168. package/dist/StoreKit/index.d.ts +1 -0
  169. package/dist/StoreKit/index.js +1 -0
  170. package/dist/VideoToolbox/functions.d.ts +81 -0
  171. package/dist/VideoToolbox/functions.js +236 -0
  172. package/dist/VideoToolbox/index.d.ts +1 -0
  173. package/dist/VideoToolbox/index.js +1 -0
  174. package/dist/Vision/functions.d.ts +16 -0
  175. package/dist/Vision/functions.js +38 -0
  176. package/dist/Vision/index.d.ts +1 -0
  177. package/dist/Vision/index.js +1 -0
  178. package/generator/ast-parser.ts +1368 -0
  179. package/generator/clang.ts +167 -0
  180. package/generator/custom.ts +936 -0
  181. package/generator/discover.ts +111 -0
  182. package/generator/emitter.ts +2026 -0
  183. package/generator/frameworks.ts +135 -0
  184. package/generator/index.ts +1334 -0
  185. package/generator/parse-worker.ts +263 -0
  186. package/generator/resolve-strings.ts +121 -0
  187. package/generator/struct-fields.ts +46 -0
  188. package/generator/templates/bind.ts +100 -0
  189. package/generator/templates/helpers.ts +70 -0
  190. package/generator/templates/nsdata.ts +97 -0
  191. package/generator/templates/osversion.ts +91 -0
  192. package/generator/type-mapper.ts +615 -0
  193. package/generator/worker-pool.ts +309 -0
  194. package/package.json +13 -4
@@ -0,0 +1,1334 @@
1
+ /**
2
+ * Main generator CLI — orchestrates the full pipeline:
3
+ * 1. Discover frameworks and scan headers for class/protocol names
4
+ * 2. Parse headers in parallel via worker threads (clang AST dump + parsing)
5
+ * 3. Collect results and build shared type-mapping state
6
+ * 4. Emit .ts declaration files
7
+ */
8
+
9
+ import { mkdir, writeFile, readdir, copyFile, unlink } from "fs/promises";
10
+ import { existsSync } from "fs";
11
+ import { join } from "path";
12
+ import {
13
+ discoverAllFrameworks,
14
+ getHeaderPath,
15
+ getProtocolHeaderPath,
16
+ getEnumHeaderPath,
17
+ type FrameworkConfig
18
+ } from "./frameworks.ts";
19
+ import { discoverFramework } from "./discover.ts";
20
+ import type {
21
+ ObjCClass,
22
+ ObjCProtocol,
23
+ ObjCIntegerEnum,
24
+ ObjCStringEnum,
25
+ ObjCStruct,
26
+ ObjCStructAlias,
27
+ ObjCFunction
28
+ } from "./ast-parser.ts";
29
+ import {
30
+ setKnownClasses,
31
+ setKnownProtocols,
32
+ setProtocolConformers,
33
+ setKnownIntegerEnums,
34
+ setKnownStringEnums,
35
+ setKnownStructs,
36
+ setKnownTypedefs,
37
+ mapReturnType,
38
+ mapParamType,
39
+ STRUCT_TS_TYPES
40
+ } from "./type-mapper.ts";
41
+ import { resolveStringConstants, cleanupResolver } from "./resolve-strings.ts";
42
+ import {
43
+ emitClassFile,
44
+ emitMergedClassFile,
45
+ emitProtocolFile,
46
+ emitFrameworkIndex,
47
+ emitTopLevelIndex,
48
+ emitDelegatesFile,
49
+ emitStructFile,
50
+ emitStructIndex,
51
+ emitIntegerEnumFile,
52
+ emitStringEnumFile,
53
+ emitFunctionsFile,
54
+ groupCaseCollisions
55
+ } from "./emitter.ts";
56
+ import type { StructDef, StructFieldDef } from "./emitter.ts";
57
+ import { parseKnownStructFields } from "./struct-fields.ts";
58
+ import { WorkerPool } from "./worker-pool.ts";
59
+ import type { UnifiedParseResult } from "./worker-pool.ts";
60
+
61
+ const SRC_DIR = join(import.meta.dir, "..", "src");
62
+
63
+ async function main(): Promise<void> {
64
+ console.log("=== objcjs-types generator ===\n");
65
+ const globalStart = performance.now();
66
+
67
+ // --- Parse CLI args: optional framework name filter ---
68
+ // Usage: bun run generate [Framework1 Framework2 ...]
69
+ // If no names given, all frameworks are regenerated.
70
+ const filterNames = process.argv
71
+ .slice(2)
72
+ .map((s) => s.trim())
73
+ .filter(Boolean);
74
+ const isFiltered = filterNames.length > 0;
75
+ if (isFiltered) {
76
+ console.log(`Regenerating frameworks: ${filterNames.join(", ")}\n`);
77
+ }
78
+
79
+ // ========================================
80
+ // Phase 1: Discovery
81
+ // ========================================
82
+
83
+ // --- Framework discovery: find all ObjC frameworks in the SDK ---
84
+ console.log("Discovering frameworks from SDK...");
85
+ const allBases = await discoverAllFrameworks();
86
+ console.log(` Found ${allBases.length} frameworks with headers\n`);
87
+
88
+ // --- Class/protocol discovery: scan headers for each framework ---
89
+ console.log("Discovering classes and protocols from headers...");
90
+ const frameworks: FrameworkConfig[] = [];
91
+
92
+ // Discover all frameworks in parallel for faster scanning
93
+ const discoveryResults = await Promise.all(
94
+ allBases.map(async (base) => {
95
+ const discovery = await discoverFramework(base.headersPath);
96
+ return { base, discovery };
97
+ })
98
+ );
99
+
100
+ for (const { base, discovery } of discoveryResults) {
101
+ // Filter out protocols whose names clash with class names (e.g., NSObject)
102
+ // to avoid generating both a class type and protocol interface with the same _Name.
103
+ for (const protoName of discovery.protocols.keys()) {
104
+ if (discovery.classes.has(protoName)) {
105
+ discovery.protocols.delete(protoName);
106
+ }
107
+ }
108
+
109
+ // Add extraHeaders classes to the discovered set. Some classes (e.g., NSObject)
110
+ // are declared in runtime headers outside the framework's Headers/ directory,
111
+ // so header scanning won't find them. They still need to be in the class list
112
+ // for emission and for allKnownClasses (used by the emitter to resolve superclasses).
113
+ if (base.extraHeaders) {
114
+ for (const className of Object.keys(base.extraHeaders)) {
115
+ if (!discovery.classes.has(className)) {
116
+ discovery.classes.set(className, className);
117
+ }
118
+ }
119
+ }
120
+
121
+ // Skip frameworks with no ObjC classes, protocols, or enums
122
+ if (
123
+ discovery.classes.size === 0 &&
124
+ discovery.protocols.size === 0 &&
125
+ discovery.integerEnums.size === 0 &&
126
+ discovery.stringEnums.size === 0
127
+ )
128
+ continue;
129
+
130
+ const fw: FrameworkConfig = {
131
+ ...base,
132
+ classes: [...discovery.classes.keys()].sort(),
133
+ protocols: [...discovery.protocols.keys()].sort(),
134
+ integerEnums: [...discovery.integerEnums.keys()].sort(),
135
+ stringEnums: [...discovery.stringEnums.keys()].sort(),
136
+ classHeaders: discovery.classes,
137
+ protocolHeaders: discovery.protocols,
138
+ integerEnumHeaders: discovery.integerEnums,
139
+ stringEnumHeaders: discovery.stringEnums
140
+ };
141
+ frameworks.push(fw);
142
+
143
+ const enumCount = fw.integerEnums.length + fw.stringEnums.length;
144
+ console.log(` ${fw.name}: ${fw.classes.length} classes, ${fw.protocols.length} protocols, ${enumCount} enums`);
145
+ }
146
+ const discoveryTime = ((performance.now() - globalStart) / 1000).toFixed(1);
147
+ console.log(` Discovery completed in ${discoveryTime}s\n`);
148
+
149
+ // Collect all known classes across all frameworks
150
+ const allKnownClasses = new Set<string>();
151
+ for (const fw of frameworks) {
152
+ for (const cls of fw.classes) {
153
+ allKnownClasses.add(cls);
154
+ }
155
+ }
156
+ setKnownClasses(allKnownClasses);
157
+
158
+ // Collect all known protocol names across all frameworks
159
+ const allKnownProtocols = new Set<string>();
160
+ for (const fw of frameworks) {
161
+ for (const proto of fw.protocols) {
162
+ allKnownProtocols.add(proto);
163
+ }
164
+ }
165
+ setKnownProtocols(allKnownProtocols);
166
+
167
+ // Collect all known enum names across all frameworks
168
+ const allKnownIntegerEnums = new Set<string>();
169
+ const allKnownStringEnums = new Set<string>();
170
+ for (const fw of frameworks) {
171
+ for (const name of fw.integerEnums) {
172
+ allKnownIntegerEnums.add(name);
173
+ }
174
+ for (const name of fw.stringEnums) {
175
+ allKnownStringEnums.add(name);
176
+ }
177
+ }
178
+ setKnownIntegerEnums(allKnownIntegerEnums);
179
+ setKnownStringEnums(allKnownStringEnums);
180
+
181
+ // Validate filter names against discovered frameworks
182
+ if (isFiltered) {
183
+ const allNames = new Set(frameworks.map((fw) => fw.name));
184
+ const invalid = filterNames.filter((n) => !allNames.has(n));
185
+ if (invalid.length > 0) {
186
+ console.error(`Unknown framework(s): ${invalid.join(", ")}`);
187
+ console.error(`Available: ${[...allNames].sort().join(", ")}`);
188
+ process.exit(1);
189
+ }
190
+ }
191
+
192
+ // Determine which frameworks to process (parse + emit)
193
+ const filterSet = new Set(filterNames);
194
+ const frameworksToProcess = isFiltered ? frameworks.filter((fw) => filterSet.has(fw.name)) : frameworks;
195
+
196
+ // ========================================
197
+ // Phase 2: Build batched parse tasks (one per framework)
198
+ // ========================================
199
+ // Instead of spawning a separate clang process per header file, we batch
200
+ // ALL headers within each framework into a single clang invocation. This
201
+ // reduces ~3400 clang processes to ~100 (one per framework), dramatically
202
+ // cutting startup overhead and parse time.
203
+
204
+ /** Batched task: all parse targets for an entire framework */
205
+ interface BatchTask {
206
+ frameworkName: string;
207
+ headerPaths: string[];
208
+ classTargets: string[];
209
+ protocolTargets: string[];
210
+ integerEnumTargets: string[];
211
+ stringEnumTargets: string[];
212
+ preIncludes: string[];
213
+ }
214
+
215
+ /** Extra header tasks are still per-header (they're outside the framework) */
216
+ interface ExtraHeaderTask {
217
+ frameworkName: string;
218
+ headerPath: string;
219
+ classTargets: string[];
220
+ }
221
+
222
+ const batchTasks: BatchTask[] = [];
223
+ const extraTasks: ExtraHeaderTask[] = [];
224
+
225
+ for (const fw of frameworksToProcess) {
226
+ const preIncludes = ["Foundation/Foundation.h", ...(fw.preIncludes ?? [])];
227
+
228
+ // Include ALL .h files from the framework's Headers/ directory.
229
+ // This is critical because ObjC categories extending a class (e.g., NSObject)
230
+ // can be declared in headers that don't contain any class/protocol/enum
231
+ // declarations themselves (e.g., NSKeyValueCoding.h). With -fmodules, clang
232
+ // automatically pulls in all transitive module content, but without modules
233
+ // we must explicitly include every header.
234
+ const allFrameworkHeaders = (await readdir(fw.headersPath))
235
+ .filter((f) => f.endsWith(".h"))
236
+ .map((f) => join(fw.headersPath, f));
237
+
238
+ const classTargets: string[] = [];
239
+ const protocolTargets: string[] = [];
240
+ const integerEnumTargets: string[] = [];
241
+ const stringEnumTargets: string[] = [];
242
+
243
+ // --- Class targets ---
244
+ for (const className of fw.classes) {
245
+ const headerPath = getHeaderPath(fw, className);
246
+ if (!existsSync(headerPath)) {
247
+ // Extra-header-only classes (e.g., NSObject from /usr/include/objc/NSObject.h)
248
+ // may not have a framework header — that's fine, they'll be parsed from extraHeaders
249
+ if (!(fw.extraHeaders && className in fw.extraHeaders)) {
250
+ console.log(` [SKIP] Header not found: ${headerPath}`);
251
+ }
252
+ continue;
253
+ }
254
+ classTargets.push(className);
255
+ }
256
+
257
+ // --- Protocol targets ---
258
+ for (const protoName of fw.protocols) {
259
+ const headerPath = getProtocolHeaderPath(fw, protoName);
260
+ if (!existsSync(headerPath)) {
261
+ console.log(` [SKIP] Protocol header not found: ${headerPath}`);
262
+ continue;
263
+ }
264
+ protocolTargets.push(protoName);
265
+ }
266
+
267
+ // --- Integer enum targets ---
268
+ for (const enumName of fw.integerEnums) {
269
+ const headerPath = getEnumHeaderPath(fw, enumName, "integer");
270
+ if (!existsSync(headerPath)) {
271
+ console.log(` [SKIP] Enum header not found: ${headerPath}`);
272
+ continue;
273
+ }
274
+ integerEnumTargets.push(enumName);
275
+ }
276
+
277
+ // --- String enum targets ---
278
+ for (const enumName of fw.stringEnums) {
279
+ const headerPath = getEnumHeaderPath(fw, enumName, "string");
280
+ if (!existsSync(headerPath)) {
281
+ console.log(` [SKIP] Enum header not found: ${headerPath}`);
282
+ continue;
283
+ }
284
+ stringEnumTargets.push(enumName);
285
+ }
286
+
287
+ // Only create a batch task if there are targets to parse
288
+ if (
289
+ allFrameworkHeaders.length > 0 &&
290
+ (classTargets.length > 0 ||
291
+ protocolTargets.length > 0 ||
292
+ integerEnumTargets.length > 0 ||
293
+ stringEnumTargets.length > 0)
294
+ ) {
295
+ batchTasks.push({
296
+ frameworkName: fw.name,
297
+ headerPaths: allFrameworkHeaders,
298
+ classTargets,
299
+ protocolTargets,
300
+ integerEnumTargets,
301
+ stringEnumTargets,
302
+ preIncludes
303
+ });
304
+ }
305
+
306
+ // --- Extra header tasks (e.g., runtime NSObject.h for alloc/init) ---
307
+ if (fw.extraHeaders) {
308
+ for (const [className, headerPath] of Object.entries(fw.extraHeaders)) {
309
+ if (!existsSync(headerPath)) {
310
+ console.log(` [SKIP] Extra header not found: ${headerPath}`);
311
+ continue;
312
+ }
313
+ extraTasks.push({
314
+ frameworkName: fw.name,
315
+ headerPath,
316
+ classTargets: [className]
317
+ });
318
+ }
319
+ }
320
+ }
321
+
322
+ // ========================================
323
+ // Phase 3: Parallel parsing via worker pool
324
+ // ========================================
325
+
326
+ // Native JSON.parse is ~3x faster than any JS-based streaming parser, but
327
+ // each clang batch materializes ~1-2GB of JS objects. With 8 workers the
328
+ // peak memory is higher but parse time drops to ~30s (from ~55s with 4).
329
+ // The actual per-task bottleneck is clang execution + JSON.parse (~95% of
330
+ // task time), not the AST walk passes (~5%). Any Xcode-capable Mac with
331
+ // 32+GB RAM handles 8 workers comfortably.
332
+ const cpuCount = navigator.hardwareConcurrency ?? 4;
333
+ const poolSize = Math.min(cpuCount, 8);
334
+ const pool = new WorkerPool(poolSize);
335
+ const totalBatchTasks = batchTasks.length;
336
+ const totalExtraTasks = extraTasks.length;
337
+ const totalTasks = totalBatchTasks + totalExtraTasks;
338
+ let completedTasks = 0;
339
+
340
+ const totalHeaders = batchTasks.reduce((sum, t) => sum + t.headerPaths.length, 0) + totalExtraTasks;
341
+
342
+ // Sort batch tasks largest-first so expensive frameworks (AppKit, Foundation)
343
+ // start immediately and don't create tail latency at the end.
344
+ batchTasks.sort((a, b) => b.headerPaths.length - a.headerPaths.length);
345
+
346
+ console.log(
347
+ `Parsing ${totalHeaders} headers via ${totalBatchTasks} batched framework tasks + ${totalExtraTasks} extra tasks using ${pool.size} worker threads...`
348
+ );
349
+
350
+ const startTime = performance.now();
351
+
352
+ /** Update the progress counter on the current console line. */
353
+ const trackProgress = (label: string) => {
354
+ completedTasks++;
355
+ process.stdout.write(`\r Progress: ${completedTasks}/${totalTasks} tasks (${label})`);
356
+ };
357
+
358
+ // Dispatch batched framework tasks
359
+ const batchPromises = batchTasks.map((task) =>
360
+ pool
361
+ .parseBatch(
362
+ task.headerPaths,
363
+ task.classTargets,
364
+ task.protocolTargets,
365
+ task.integerEnumTargets,
366
+ task.stringEnumTargets,
367
+ task.preIncludes,
368
+ task.frameworkName
369
+ )
370
+ .then((result) => {
371
+ trackProgress(task.frameworkName);
372
+ return { task, result, error: null as string | null, isExtra: false as const };
373
+ })
374
+ .catch((err) => {
375
+ trackProgress(task.frameworkName);
376
+ return { task, result: null as UnifiedParseResult | null, error: String(err), isExtra: false as const };
377
+ })
378
+ );
379
+
380
+ // Dispatch extra header tasks (still per-header, using parseAll)
381
+ const extraPromises = extraTasks.map((task) =>
382
+ pool
383
+ .parseAll(
384
+ task.headerPath,
385
+ task.classTargets,
386
+ [], // no protocols
387
+ [], // no integer enums
388
+ [], // no string enums
389
+ undefined // no fallback pre-includes for extra headers
390
+ )
391
+ .then((result) => {
392
+ trackProgress(`extra:${task.classTargets[0]}`);
393
+ return { task, result, error: null as string | null, isExtra: true as const };
394
+ })
395
+ .catch((err) => {
396
+ trackProgress(`extra:${task.classTargets[0]}`);
397
+ return { task, result: null as UnifiedParseResult | null, error: String(err), isExtra: true as const };
398
+ })
399
+ );
400
+
401
+ // Wait for all parse tasks to complete
402
+ const allResults = await Promise.all([...batchPromises, ...extraPromises]);
403
+
404
+ pool.destroy();
405
+
406
+ const parseTime = ((performance.now() - startTime) / 1000).toFixed(1);
407
+ process.stdout.write(`\r Parsed ${totalHeaders} headers in ${parseTime}s \n\n`);
408
+
409
+ // ========================================
410
+ // Phase 4: Collect results and build shared state
411
+ // ========================================
412
+
413
+ // Organize parsed classes by framework
414
+ const frameworkClasses = new Map<string, Map<string, ObjCClass>>();
415
+ const allParsedClasses = new Map<string, ObjCClass>();
416
+
417
+ // First pass: regular (non-extra) results
418
+ for (const entry of allResults) {
419
+ if (entry.isExtra) continue; // Handle in second pass
420
+ if (entry.error) {
421
+ console.log(` [ERROR] ${entry.task.frameworkName}: ${entry.error}`);
422
+ continue;
423
+ }
424
+ if (!entry.result) continue;
425
+
426
+ // --- Classes ---
427
+ if (entry.result.classes.size > 0) {
428
+ if (!frameworkClasses.has(entry.task.frameworkName)) {
429
+ frameworkClasses.set(entry.task.frameworkName, new Map());
430
+ }
431
+ const fwClasses = frameworkClasses.get(entry.task.frameworkName)!;
432
+ for (const [name, cls] of entry.result.classes) {
433
+ fwClasses.set(name, cls);
434
+ allParsedClasses.set(name, cls);
435
+ }
436
+ }
437
+ }
438
+
439
+ // Second pass: extra header results (merge into existing classes)
440
+ for (const entry of allResults) {
441
+ if (!entry.isExtra) continue;
442
+ if (entry.error) {
443
+ console.log(` [ERROR] Extra header ${entry.task.headerPath}: ${entry.error}`);
444
+ continue;
445
+ }
446
+ if (!entry.result) continue;
447
+
448
+ if (!frameworkClasses.has(entry.task.frameworkName)) {
449
+ frameworkClasses.set(entry.task.frameworkName, new Map());
450
+ }
451
+ const fwClasses = frameworkClasses.get(entry.task.frameworkName)!;
452
+
453
+ for (const [className, extraCls] of entry.result.classes) {
454
+ const existing = fwClasses.get(className);
455
+ if (existing) {
456
+ // Merge: add methods/properties from extra header that don't already exist
457
+ const existingInstanceSelectors = new Set(existing.instanceMethods.map((m) => m.selector));
458
+ const existingClassSelectors = new Set(existing.classMethods.map((m) => m.selector));
459
+ const existingPropertyNames = new Set(existing.properties.map((p) => p.name));
460
+
461
+ for (const m of extraCls.instanceMethods) {
462
+ if (!existingInstanceSelectors.has(m.selector)) {
463
+ existing.instanceMethods.push(m);
464
+ }
465
+ }
466
+ for (const m of extraCls.classMethods) {
467
+ if (!existingClassSelectors.has(m.selector)) {
468
+ existing.classMethods.push(m);
469
+ }
470
+ }
471
+ for (const p of extraCls.properties) {
472
+ if (!existingPropertyNames.has(p.name)) {
473
+ existing.properties.push(p);
474
+ }
475
+ }
476
+
477
+ console.log(
478
+ ` [extra] Merged ${className}: +${extraCls.instanceMethods.length} instance, ` +
479
+ `+${extraCls.classMethods.length} class methods, +${extraCls.properties.length} props`
480
+ );
481
+ } else {
482
+ fwClasses.set(className, extraCls);
483
+ allParsedClasses.set(className, extraCls);
484
+ console.log(` [extra] Added ${className}`);
485
+ }
486
+ }
487
+ }
488
+
489
+ // Organize parsed protocols by framework
490
+ const frameworkProtocolsParsed = new Map<string, Map<string, ObjCProtocol>>();
491
+ for (const entry of allResults) {
492
+ if (entry.isExtra) continue;
493
+ if (entry.error || !entry.result) continue;
494
+ if (entry.result.protocols.size === 0) continue;
495
+
496
+ if (!frameworkProtocolsParsed.has(entry.task.frameworkName)) {
497
+ frameworkProtocolsParsed.set(entry.task.frameworkName, new Map());
498
+ }
499
+ const fwProtos = frameworkProtocolsParsed.get(entry.task.frameworkName)!;
500
+ for (const [name, proto] of entry.result.protocols) {
501
+ fwProtos.set(name, proto);
502
+ }
503
+ }
504
+
505
+ // Organize parsed enums by framework
506
+ const frameworkIntegerEnums = new Map<string, Map<string, ObjCIntegerEnum>>();
507
+ const frameworkStringEnums = new Map<string, Map<string, ObjCStringEnum>>();
508
+ for (const entry of allResults) {
509
+ if (entry.isExtra) continue;
510
+ if (entry.error || !entry.result) continue;
511
+ if (entry.result.integerEnums.size === 0 && entry.result.stringEnums.size === 0) continue;
512
+
513
+ if (!frameworkIntegerEnums.has(entry.task.frameworkName)) {
514
+ frameworkIntegerEnums.set(entry.task.frameworkName, new Map());
515
+ }
516
+ if (!frameworkStringEnums.has(entry.task.frameworkName)) {
517
+ frameworkStringEnums.set(entry.task.frameworkName, new Map());
518
+ }
519
+
520
+ const fwIntEnums = frameworkIntegerEnums.get(entry.task.frameworkName)!;
521
+ const fwStrEnums = frameworkStringEnums.get(entry.task.frameworkName)!;
522
+
523
+ for (const [name, enumDef] of entry.result.integerEnums) {
524
+ fwIntEnums.set(name, enumDef);
525
+ }
526
+ for (const [name, enumDef] of entry.result.stringEnums) {
527
+ fwStrEnums.set(name, enumDef);
528
+ }
529
+ }
530
+
531
+ // Organize parsed functions by framework
532
+ const frameworkFunctions = new Map<string, Map<string, ObjCFunction>>();
533
+ for (const entry of allResults) {
534
+ if (entry.isExtra) continue;
535
+ if (entry.error || !entry.result) continue;
536
+ if (entry.result.functions.size === 0) continue;
537
+
538
+ if (!frameworkFunctions.has(entry.task.frameworkName)) {
539
+ frameworkFunctions.set(entry.task.frameworkName, new Map());
540
+ }
541
+ const fwFuncs = frameworkFunctions.get(entry.task.frameworkName)!;
542
+ for (const [name, func] of entry.result.functions) {
543
+ fwFuncs.set(name, func);
544
+ }
545
+ }
546
+
547
+ // Collect all parsed structs and struct aliases across all headers
548
+ const allParsedStructs = new Map<string, ObjCStruct>();
549
+ const allStructAliases: ObjCStructAlias[] = [];
550
+ for (const entry of allResults) {
551
+ if (entry.isExtra) continue;
552
+ if (entry.error || !entry.result) continue;
553
+
554
+ for (const [name, structDef] of entry.result.structs) {
555
+ // Keep the first definition encountered (later duplicates are typically
556
+ // forward declarations or re-declarations of the same struct)
557
+ if (!allParsedStructs.has(name)) {
558
+ allParsedStructs.set(name, structDef);
559
+ }
560
+ }
561
+ for (const alias of entry.result.structAliases) {
562
+ // Deduplicate aliases (same alias may appear in multiple headers)
563
+ if (!allStructAliases.some((a) => a.name === alias.name)) {
564
+ allStructAliases.push(alias);
565
+ }
566
+ }
567
+ }
568
+
569
+ // Collect all typedefs across all headers for general typedef resolution
570
+ const allTypedefs = new Map<string, string>();
571
+ for (const entry of allResults) {
572
+ if (entry.isExtra) continue;
573
+ if (entry.error || !entry.result) continue;
574
+ for (const [name, qualType] of entry.result.typedefs) {
575
+ if (!allTypedefs.has(name)) {
576
+ allTypedefs.set(name, qualType);
577
+ }
578
+ }
579
+ }
580
+ setKnownTypedefs(allTypedefs);
581
+
582
+ // Resolve actual string values for extern NSString * constants.
583
+ // This compiles a small ObjC helper once, then invokes it per-framework
584
+ // with the symbol names discovered during parsing.
585
+ console.log("Resolving string enum values from framework binaries...");
586
+ let totalResolved = 0;
587
+ let totalStringSymbols = 0;
588
+
589
+ // Build resolution tasks
590
+ interface ResolveTask {
591
+ fwName: string;
592
+ libraryPath: string;
593
+ symbols: string[];
594
+ }
595
+
596
+ const resolveTasks: ResolveTask[] = [];
597
+
598
+ for (const fw of frameworksToProcess) {
599
+ const fwStrEnums = frameworkStringEnums.get(fw.name);
600
+ if (!fwStrEnums || fwStrEnums.size === 0) continue;
601
+
602
+ // Collect all symbol names for this framework
603
+ const allSymbols: string[] = [];
604
+ for (const enumDef of fwStrEnums.values()) {
605
+ for (const v of enumDef.values) {
606
+ allSymbols.push(v.symbolName);
607
+ }
608
+ }
609
+ if (allSymbols.length === 0) continue;
610
+
611
+ resolveTasks.push({ fwName: fw.name, libraryPath: fw.libraryPath, symbols: allSymbols });
612
+ totalStringSymbols += allSymbols.length;
613
+ }
614
+
615
+ // Process resolution tasks sequentially to avoid spawning too many
616
+ // concurrent child processes. Each task is fast (~100ms) since dlopen/dlsym
617
+ // are lightweight. The timeout in resolveStringConstants guards against
618
+ // frameworks whose dlopen hangs.
619
+ for (const task of resolveTasks) {
620
+ try {
621
+ const resolved = await resolveStringConstants(task.libraryPath, task.symbols);
622
+ const fwStrEnums = frameworkStringEnums.get(task.fwName);
623
+ if (fwStrEnums) {
624
+ for (const enumDef of fwStrEnums.values()) {
625
+ for (const v of enumDef.values) {
626
+ const value = resolved.get(v.symbolName);
627
+ if (value !== undefined) {
628
+ v.value = value;
629
+ totalResolved++;
630
+ }
631
+ }
632
+ }
633
+ }
634
+ } catch (err) {
635
+ console.log(` [WARN] Failed to resolve string values for ${task.fwName}: ${err}`);
636
+ }
637
+ }
638
+
639
+ console.log(` Resolved ${totalResolved}/${totalStringSymbols} string enum values\n`);
640
+
641
+ // Protocol conformer map is built later, after allParsedProtocols is available,
642
+ // so we can expand conformances transitively through the protocol hierarchy.
643
+
644
+ // Print parse summary per framework
645
+ for (const fw of frameworksToProcess) {
646
+ const fwClasses = frameworkClasses.get(fw.name);
647
+ const fwProtos = frameworkProtocolsParsed.get(fw.name);
648
+ const fwIntEnums = frameworkIntegerEnums.get(fw.name);
649
+ const fwStrEnums = frameworkStringEnums.get(fw.name);
650
+ const classCount = fwClasses?.size ?? 0;
651
+ const protoCount = fwProtos?.size ?? 0;
652
+ const intEnumCount = fwIntEnums?.size ?? 0;
653
+ const strEnumCount = fwStrEnums?.size ?? 0;
654
+ const totalEnums = intEnumCount + strEnumCount;
655
+ const expectedEnums = fw.integerEnums.length + fw.stringEnums.length;
656
+ console.log(
657
+ ` ${fw.name}: parsed ${classCount}/${fw.classes.length} classes, ` +
658
+ `${protoCount}/${fw.protocols.length} protocols, ` +
659
+ `${totalEnums}/${expectedEnums} enums`
660
+ );
661
+ }
662
+ console.log("");
663
+
664
+ // Update knownClasses/knownProtocols to reflect only actually-parsed entities.
665
+ // Some classes are discovered in headers but fail to parse (e.g., classes hidden
666
+ // behind NS_REFINED_FOR_SWIFT that clang omits from the AST). If we leave
667
+ // discovered-but-unparsed classes in the known set, the type-mapper will generate
668
+ // _ClassName references to types that have no corresponding .ts file, causing
669
+ // broken imports. By narrowing to parsed classes only, those types fall back to
670
+ // NobjcObject instead.
671
+ //
672
+ // For filtered runs (only regenerating some frameworks), we keep discovered classes
673
+ // from non-processed frameworks since their .ts files already exist on disk from
674
+ // a previous full run.
675
+ const parsedClassNames = new Set(allParsedClasses.keys());
676
+ const allParsedProtocolNames = new Set<string>();
677
+ const allParsedProtocols = new Map<string, ObjCProtocol>();
678
+ for (const fwProtos of frameworkProtocolsParsed.values()) {
679
+ for (const [name, proto] of fwProtos) {
680
+ allParsedProtocolNames.add(name);
681
+ allParsedProtocols.set(name, proto);
682
+ }
683
+ }
684
+
685
+ // Add discovered classes/protocols from frameworks we're NOT processing
686
+ // (their .ts files exist from a previous run)
687
+ const processedFrameworkNames = new Set(frameworksToProcess.map((fw) => fw.name));
688
+ for (const fw of frameworks) {
689
+ if (processedFrameworkNames.has(fw.name)) continue;
690
+ for (const cls of fw.classes) {
691
+ parsedClassNames.add(cls);
692
+ }
693
+ for (const proto of fw.protocols) {
694
+ allParsedProtocolNames.add(proto);
695
+ }
696
+ }
697
+
698
+ setKnownClasses(parsedClassNames);
699
+ setKnownProtocols(allParsedProtocolNames);
700
+
701
+ // Build protocol -> conforming classes map from all parsed classes.
702
+ // This must happen after ALL parsing completes so every conformance is known.
703
+ // We also expand conformances transitively through the protocol hierarchy:
704
+ // if class X conforms to protocol A, and A extends B, then X also conforms to B.
705
+ // This ensures that return type union expansion (e.g., credential() returning a
706
+ // union of all ASAuthorizationCredential-conforming classes) includes classes that
707
+ // conform indirectly through protocol inheritance chains.
708
+ const protocolConformers = new Map<string, Set<string>>();
709
+ for (const [className, cls] of allParsedClasses) {
710
+ if (!parsedClassNames.has(className)) continue;
711
+ for (const protoName of cls.protocols) {
712
+ if (!protocolConformers.has(protoName)) {
713
+ protocolConformers.set(protoName, new Set());
714
+ }
715
+ protocolConformers.get(protoName)!.add(className);
716
+ }
717
+ }
718
+
719
+ // Expand conformances transitively: walk each protocol's extendedProtocols chain
720
+ // and propagate all conforming classes upward.
721
+ function getTransitiveParentProtocols(protoName: string, visited: Set<string>): string[] {
722
+ if (visited.has(protoName)) return [];
723
+ visited.add(protoName);
724
+ const proto = allParsedProtocols.get(protoName);
725
+ if (!proto) return [];
726
+ const parents: string[] = [];
727
+ for (const parent of proto.extendedProtocols) {
728
+ parents.push(parent);
729
+ parents.push(...getTransitiveParentProtocols(parent, visited));
730
+ }
731
+ return parents;
732
+ }
733
+
734
+ // For each protocol that has direct conformers, propagate those conformers
735
+ // to all ancestor protocols in the hierarchy.
736
+ for (const [protoName, conformers] of [...protocolConformers]) {
737
+ const ancestors = getTransitiveParentProtocols(protoName, new Set());
738
+ for (const ancestor of ancestors) {
739
+ if (!protocolConformers.has(ancestor)) {
740
+ protocolConformers.set(ancestor, new Set());
741
+ }
742
+ for (const cls of conformers) {
743
+ protocolConformers.get(ancestor)!.add(cls);
744
+ }
745
+ }
746
+ }
747
+
748
+ setProtocolConformers(protocolConformers);
749
+
750
+ // ========================================
751
+ // Phase 4b: Build struct definitions from parsed AST + KNOWN_STRUCT_FIELDS
752
+ // ========================================
753
+
754
+ // Parse the runtime's known struct field table to determine which structs
755
+ // get named fields vs positional (field0, field1, ...) names at JS level.
756
+ const knownStructFields = await parseKnownStructFields();
757
+ console.log(` Parsed ${knownStructFields.size} struct field mappings from objc-js runtime`);
758
+ console.log(` Found ${allParsedStructs.size} struct definitions, ${allStructAliases.length} aliases from AST\n`);
759
+
760
+ // Only emit structs that are referenced by class methods/properties.
761
+ // This is determined by STRUCT_TYPE_MAP — we build it from discovered structs,
762
+ // but only include structs that are actually used.
763
+
764
+ // Build the struct name sets for setKnownStructs()
765
+ const structNames = new Set(allParsedStructs.keys());
766
+ const aliasMap = new Map(allStructAliases.map((a) => [a.name, a.target]));
767
+ const internalNameMap = new Map<string, string>();
768
+ for (const [name, structDef] of allParsedStructs) {
769
+ if (structDef.internalName) {
770
+ internalNameMap.set(name, structDef.internalName);
771
+ }
772
+ }
773
+ setKnownStructs(structNames, aliasMap, internalNameMap);
774
+
775
+ // --- Scan all parsed classes & protocols to find which structs are actually referenced ---
776
+ // We temporarily registered ALL structs in STRUCT_TYPE_MAP above so that mapReturnType/
777
+ // mapParamType can resolve struct qualType strings. Now we scan every method/property
778
+ // signature to see which struct TS types actually appear, then filter down to only those.
779
+ const referencedStructTSNames = new Set<string>();
780
+
781
+ function extractStructRefs(typeStr: string): void {
782
+ for (const structName of STRUCT_TS_TYPES) {
783
+ if (typeStr === structName || typeStr.startsWith(structName + " ")) {
784
+ referencedStructTSNames.add(structName);
785
+ }
786
+ }
787
+ }
788
+
789
+ // Scan all classes
790
+ for (const cls of allParsedClasses.values()) {
791
+ for (const method of [...cls.instanceMethods, ...cls.classMethods]) {
792
+ extractStructRefs(mapReturnType(method.returnType, cls.name));
793
+ for (const param of method.parameters) {
794
+ extractStructRefs(mapParamType(param.type, cls.name));
795
+ }
796
+ }
797
+ for (const prop of cls.properties) {
798
+ extractStructRefs(mapReturnType(prop.type, cls.name));
799
+ extractStructRefs(mapParamType(prop.type, cls.name));
800
+ }
801
+ }
802
+
803
+ // Scan all protocols
804
+ for (const fwProtos of frameworkProtocolsParsed.values()) {
805
+ for (const proto of fwProtos.values()) {
806
+ for (const method of [...proto.instanceMethods, ...proto.classMethods]) {
807
+ extractStructRefs(mapReturnType(method.returnType, proto.name));
808
+ for (const param of method.parameters) {
809
+ extractStructRefs(mapParamType(param.type, proto.name));
810
+ }
811
+ }
812
+ for (const prop of proto.properties) {
813
+ extractStructRefs(mapReturnType(prop.type, proto.name));
814
+ extractStructRefs(mapParamType(prop.type, proto.name));
815
+ }
816
+ }
817
+ }
818
+
819
+ // Also include transitive struct dependencies (e.g., CGRect references CGPoint, CGSize)
820
+ function addStructDeps(name: string): void {
821
+ const structDef = allParsedStructs.get(name);
822
+ if (!structDef) return;
823
+
824
+ const runtimeFields =
825
+ knownStructFields.get(name) ??
826
+ (structDef.internalName ? knownStructFields.get(structDef.internalName) : undefined);
827
+
828
+ if (runtimeFields && runtimeFields.length === structDef.fields.length) {
829
+ for (const field of structDef.fields) {
830
+ const fieldTypeCleaned = field.type.replace(/^(const\s+)?struct\s+/, "").trim();
831
+ if (structNames.has(fieldTypeCleaned) && !referencedStructTSNames.has(fieldTypeCleaned)) {
832
+ referencedStructTSNames.add(fieldTypeCleaned);
833
+ addStructDeps(fieldTypeCleaned);
834
+ }
835
+ const aliasTarget = aliasMap.get(fieldTypeCleaned);
836
+ if (aliasTarget && !referencedStructTSNames.has(aliasTarget)) {
837
+ referencedStructTSNames.add(aliasTarget);
838
+ addStructDeps(aliasTarget);
839
+ }
840
+ }
841
+ }
842
+ }
843
+
844
+ for (const name of [...referencedStructTSNames]) {
845
+ addStructDeps(name);
846
+ }
847
+
848
+ // Filter: only keep parsed structs that are referenced
849
+ const filteredStructs = new Map<string, ObjCStruct>();
850
+ for (const [name, structDef] of allParsedStructs) {
851
+ if (referencedStructTSNames.has(name)) {
852
+ filteredStructs.set(name, structDef);
853
+ }
854
+ }
855
+
856
+ // Filter aliases: only keep those whose target is referenced
857
+ const filteredAliases = allStructAliases.filter((a) => referencedStructTSNames.has(a.target));
858
+
859
+ // Re-register with only the filtered structs so STRUCT_TYPE_MAP doesn't
860
+ // contain random C structs like siginfo_t
861
+ const filteredStructNames = new Set(filteredStructs.keys());
862
+ const filteredAliasMap = new Map(filteredAliases.map((a) => [a.name, a.target]));
863
+ const filteredInternalNameMap = new Map<string, string>();
864
+ for (const [name, structDef] of filteredStructs) {
865
+ if (structDef.internalName) {
866
+ filteredInternalNameMap.set(name, structDef.internalName);
867
+ }
868
+ }
869
+ setKnownStructs(filteredStructNames, filteredAliasMap, filteredInternalNameMap);
870
+
871
+ console.log(` Filtered to ${filteredStructs.size} referenced structs + ${filteredAliases.length} aliases\n`);
872
+
873
+ /**
874
+ * Map an ObjC field type string to a TS type.
875
+ * Struct fields that are other structs get the struct interface name;
876
+ * everything else maps to "number" (CGFloat, int, NSInteger, etc.).
877
+ */
878
+ function mapStructFieldType(cType: string): string {
879
+ // Clean up the C type: remove "struct " prefix, const, etc.
880
+ const cleaned = cType.replace(/^(const\s+)?struct\s+/, "").trim();
881
+ // Check if this field references another known struct
882
+ if (filteredStructNames.has(cleaned)) return cleaned;
883
+ // Check aliases
884
+ const aliasTarget = filteredAliasMap.get(cleaned);
885
+ if (aliasTarget) return aliasTarget;
886
+ // Everything else is numeric
887
+ return "number";
888
+ }
889
+
890
+ /**
891
+ * Build the StructDef array from parsed AST data cross-referenced with
892
+ * the runtime's KNOWN_STRUCT_FIELDS table.
893
+ *
894
+ * For structs in KNOWN_STRUCT_FIELDS: use named fields from that table.
895
+ * For structs NOT in the table: use positional field0, field1, ... names.
896
+ * For nested struct fields: auto-generate flattened factory params and body.
897
+ */
898
+ function buildStructDefs(): StructDef[] {
899
+ const defs: StructDef[] = [];
900
+ const emittedStructs = new Set<string>();
901
+
902
+ /**
903
+ * Ensure a struct's dependencies are emitted before itself.
904
+ * Returns true if the struct was successfully processed.
905
+ */
906
+ function emitStruct(name: string): boolean {
907
+ if (emittedStructs.has(name)) return true;
908
+
909
+ const structDef = filteredStructs.get(name);
910
+ if (!structDef) return false;
911
+
912
+ // Check KNOWN_STRUCT_FIELDS for this struct.
913
+ // For NSRange, the internal struct name is _NSRange, but the runtime table
914
+ // may use either "NSRange" or "_NSRange" as the key.
915
+ const runtimeFields =
916
+ knownStructFields.get(name) ??
917
+ (structDef.internalName ? knownStructFields.get(structDef.internalName) : undefined);
918
+
919
+ const astFields = structDef.fields;
920
+
921
+ // Determine the TS fields for this struct
922
+ const tsFields: StructFieldDef[] = [];
923
+ const hasRuntimeNames = runtimeFields !== undefined;
924
+
925
+ if (hasRuntimeNames && runtimeFields.length === astFields.length) {
926
+ // Use runtime field names with AST-derived types
927
+ for (let i = 0; i < runtimeFields.length; i++) {
928
+ const fieldName = runtimeFields[i]!;
929
+ const fieldType = mapStructFieldType(astFields[i]!.type);
930
+ tsFields.push({ name: fieldName, type: fieldType });
931
+
932
+ // Ensure nested struct dependencies are emitted first
933
+ if (fieldType !== "number") {
934
+ emitStruct(fieldType);
935
+ }
936
+ }
937
+ } else {
938
+ // Positional field names (field0, field1, ...)
939
+ for (let i = 0; i < astFields.length; i++) {
940
+ tsFields.push({ name: `field${i}`, type: "number" });
941
+ }
942
+ }
943
+
944
+ // Check if any fields reference other structs (nested structs)
945
+ const hasNestedStructs = tsFields.some((f) => f.type !== "number");
946
+
947
+ emittedStructs.add(name);
948
+
949
+ if (hasNestedStructs) {
950
+ // Generate flattened factory params for nested structs.
951
+ // e.g., CGRect with fields { origin: CGPoint, size: CGSize }
952
+ // gets factory params (x, y, width, height) instead of (origin, size).
953
+ const factoryParams: { name: string; type: string }[] = [];
954
+ const bodyParts: string[] = [];
955
+
956
+ for (const field of tsFields) {
957
+ if (field.type === "number") {
958
+ factoryParams.push({ name: field.name, type: "number" });
959
+ bodyParts.push(field.name);
960
+ } else {
961
+ // Look up the nested struct's fields to flatten them
962
+ const nestedDef = defs.find((d) => !("aliasOf" in d) && d.tsName === field.type);
963
+ if (nestedDef && !("aliasOf" in nestedDef)) {
964
+ const nestedParamNames: string[] = [];
965
+ for (const nestedField of nestedDef.fields) {
966
+ factoryParams.push({ name: nestedField.name, type: nestedField.type });
967
+ nestedParamNames.push(nestedField.name);
968
+ }
969
+ bodyParts.push(`{ ${nestedParamNames.join(", ")} }`);
970
+ } else {
971
+ // Fallback: use the field name as-is
972
+ factoryParams.push({ name: field.name, type: field.type });
973
+ bodyParts.push(field.name);
974
+ }
975
+ }
976
+ }
977
+
978
+ // Build the factory body expression
979
+ const fieldAssignments = tsFields.map((f, i) => `${f.name}: ${bodyParts[i]}`);
980
+ const factoryBody = `{ ${fieldAssignments.join(", ")} }`;
981
+
982
+ defs.push({
983
+ tsName: name,
984
+ fields: tsFields,
985
+ factoryParams,
986
+ factoryBody
987
+ });
988
+ } else {
989
+ defs.push({
990
+ tsName: name,
991
+ fields: tsFields
992
+ });
993
+ }
994
+
995
+ return true;
996
+ }
997
+
998
+ // Emit only filtered (referenced) structs — dependencies handled recursively
999
+ // Sort for deterministic output order
1000
+ const sortedNames = [...filteredStructs.keys()].sort();
1001
+ for (const name of sortedNames) {
1002
+ emitStruct(name);
1003
+ }
1004
+
1005
+ // Emit aliases at the end
1006
+ const sortedAliases = [...filteredAliases].sort((a, b) => a.name.localeCompare(b.name));
1007
+ for (const alias of sortedAliases) {
1008
+ // Only emit if the target struct was emitted
1009
+ if (emittedStructs.has(alias.target)) {
1010
+ defs.push({ tsName: alias.name, aliasOf: alias.target });
1011
+ }
1012
+ }
1013
+
1014
+ return defs;
1015
+ }
1016
+
1017
+ const structDefs = buildStructDefs();
1018
+ console.log(` Built ${structDefs.length} struct definitions for emission\n`);
1019
+
1020
+ const emitStart = performance.now();
1021
+ const generatedProtocolsByFramework = new Map<string, string[]>();
1022
+
1023
+ // Build a global classToFile map across ALL frameworks for cross-framework
1024
+ // import resolution on case-sensitive filesystems.
1025
+ const globalClassToFile = new Map<string, string>();
1026
+ const collisionsByFramework = new Map<string, Map<string, string[]>>();
1027
+
1028
+ for (const framework of frameworksToProcess) {
1029
+ const collisions = groupCaseCollisions(framework.classes);
1030
+ collisionsByFramework.set(framework.name, collisions);
1031
+ for (const [canonical, group] of collisions) {
1032
+ for (const name of group) {
1033
+ globalClassToFile.set(name, canonical);
1034
+ }
1035
+ }
1036
+ }
1037
+
1038
+ for (const framework of frameworksToProcess) {
1039
+ const frameworkDir = join(SRC_DIR, framework.name);
1040
+ await mkdir(frameworkDir, { recursive: true });
1041
+
1042
+ const fwClasses = frameworkClasses.get(framework.name) ?? new Map<string, ObjCClass>();
1043
+ const fwProtos = frameworkProtocolsParsed.get(framework.name) ?? new Map<string, ObjCProtocol>();
1044
+ const fwIntEnums = frameworkIntegerEnums.get(framework.name) ?? new Map<string, ObjCIntegerEnum>();
1045
+ const fwStrEnums = frameworkStringEnums.get(framework.name) ?? new Map<string, ObjCStringEnum>();
1046
+
1047
+ // Detect case-insensitive filename collisions among this framework's classes
1048
+ const collisions = collisionsByFramework.get(framework.name)!;
1049
+ const collisionMembers = new Set<string>();
1050
+ for (const group of collisions.values()) {
1051
+ for (const name of group) {
1052
+ collisionMembers.add(name);
1053
+ }
1054
+ }
1055
+
1056
+ if (collisions.size > 0) {
1057
+ console.log(` ${framework.name}: ${collisions.size} case-collision group(s) — merging into shared files`);
1058
+ }
1059
+
1060
+ // Emit class files
1061
+ const generatedClasses: string[] = [];
1062
+ const classWritePromises: Promise<void>[] = [];
1063
+
1064
+ // First: emit merged files for collision groups
1065
+ for (const [canonical, group] of collisions) {
1066
+ // Delete stale files first — on case-insensitive filesystems (macOS APFS),
1067
+ // writing to a different-cased filename doesn't update the on-disk name.
1068
+ for (const name of group) {
1069
+ try {
1070
+ await unlink(join(frameworkDir, `${name}.ts`));
1071
+ } catch {}
1072
+ }
1073
+ const groupClasses: ObjCClass[] = [];
1074
+ for (const name of group) {
1075
+ const cls = fwClasses.get(name);
1076
+ if (cls) {
1077
+ groupClasses.push(cls);
1078
+ generatedClasses.push(name);
1079
+ }
1080
+ }
1081
+ if (groupClasses.length === 0) continue;
1082
+
1083
+ const content = emitMergedClassFile(
1084
+ groupClasses,
1085
+ framework,
1086
+ frameworks,
1087
+ parsedClassNames,
1088
+ allParsedClasses,
1089
+ allParsedProtocolNames,
1090
+ globalClassToFile,
1091
+ allParsedProtocols
1092
+ );
1093
+ classWritePromises.push(writeFile(join(frameworkDir, `${canonical}.ts`), content));
1094
+ }
1095
+
1096
+ // Then: emit single-class files for non-colliding classes
1097
+ for (const className of framework.classes) {
1098
+ if (collisionMembers.has(className)) continue;
1099
+ const cls = fwClasses.get(className);
1100
+ if (!cls) continue;
1101
+
1102
+ const content = emitClassFile(
1103
+ cls,
1104
+ framework,
1105
+ frameworks,
1106
+ parsedClassNames,
1107
+ allParsedClasses,
1108
+ allParsedProtocolNames,
1109
+ globalClassToFile,
1110
+ allParsedProtocols
1111
+ );
1112
+ classWritePromises.push(writeFile(join(frameworkDir, `${className}.ts`), content));
1113
+ generatedClasses.push(className);
1114
+ }
1115
+
1116
+ // Emit protocol files
1117
+ const generatedProtocols: string[] = [];
1118
+ const protoWritePromises: Promise<void>[] = [];
1119
+
1120
+ for (const protoName of framework.protocols) {
1121
+ const proto = fwProtos.get(protoName);
1122
+ if (!proto) continue;
1123
+
1124
+ const content = emitProtocolFile(
1125
+ proto,
1126
+ framework,
1127
+ frameworks,
1128
+ parsedClassNames,
1129
+ allParsedProtocolNames,
1130
+ globalClassToFile
1131
+ );
1132
+ protoWritePromises.push(writeFile(join(frameworkDir, `${protoName}.ts`), content));
1133
+ generatedProtocols.push(protoName);
1134
+ }
1135
+
1136
+ // Emit integer enum files
1137
+ const generatedIntegerEnums: string[] = [];
1138
+ const enumWritePromises: Promise<void>[] = [];
1139
+ const enumContents = new Map<string, string>(); // enumName -> file content
1140
+
1141
+ for (const enumName of framework.integerEnums) {
1142
+ const enumDef = fwIntEnums.get(enumName);
1143
+ if (!enumDef) continue;
1144
+
1145
+ enumContents.set(enumName, emitIntegerEnumFile(enumDef));
1146
+ generatedIntegerEnums.push(enumName);
1147
+ }
1148
+
1149
+ // Emit string enum files
1150
+ const generatedStringEnums: string[] = [];
1151
+ const generatedStringEnumsTypeOnly: string[] = [];
1152
+
1153
+ for (const enumName of framework.stringEnums) {
1154
+ const enumDef = fwStrEnums.get(enumName);
1155
+ if (!enumDef) continue;
1156
+
1157
+ enumContents.set(enumName, emitStringEnumFile(enumDef, framework.name));
1158
+
1159
+ // Enums with resolved values export a const + type; unresolved ones are type-only
1160
+ const hasResolvedValues = enumDef.values.some((v) => v.value !== null);
1161
+ if (hasResolvedValues) {
1162
+ generatedStringEnums.push(enumName);
1163
+ } else {
1164
+ generatedStringEnumsTypeOnly.push(enumName);
1165
+ }
1166
+ }
1167
+
1168
+ // Detect enum case collisions (names that produce the same filename on
1169
+ // case-insensitive filesystems like macOS HFS+/APFS)
1170
+ const allGenEnumNames = [...generatedIntegerEnums, ...generatedStringEnums, ...generatedStringEnumsTypeOnly];
1171
+ const enumCollisions = groupCaseCollisions(allGenEnumNames);
1172
+ const enumCollisionMembers = new Set<string>();
1173
+ const enumToFile = new Map<string, string>();
1174
+ for (const [canonical, group] of enumCollisions) {
1175
+ for (const name of group) {
1176
+ enumCollisionMembers.add(name);
1177
+ enumToFile.set(name, canonical);
1178
+ }
1179
+ }
1180
+
1181
+ if (enumCollisions.size > 0) {
1182
+ console.log(
1183
+ ` ${framework.name}: ${enumCollisions.size} enum case-collision group(s) — merging into shared files`
1184
+ );
1185
+ }
1186
+
1187
+ // Write merged files for enum collision groups
1188
+ for (const [canonical, group] of enumCollisions) {
1189
+ // Delete stale files first — on case-insensitive filesystems (macOS APFS),
1190
+ // writing to a different-cased filename doesn't update the on-disk name.
1191
+ for (const name of group) {
1192
+ try {
1193
+ await unlink(join(frameworkDir, `${name}.ts`));
1194
+ } catch {}
1195
+ }
1196
+ const parts: string[] = [];
1197
+ let isFirst = true;
1198
+ for (const name of group) {
1199
+ const content = enumContents.get(name);
1200
+ if (!content) continue;
1201
+ if (isFirst) {
1202
+ parts.push(content);
1203
+ isFirst = false;
1204
+ } else {
1205
+ // Strip the AUTO-GENERATED header from subsequent entries
1206
+ const withoutHeader = content.replace(/^\/\/ AUTO-GENERATED[^\n]*\n/, "");
1207
+ parts.push(withoutHeader);
1208
+ }
1209
+ }
1210
+ enumWritePromises.push(writeFile(join(frameworkDir, `${canonical}.ts`), parts.join("\n")));
1211
+ }
1212
+
1213
+ // Write individual (non-colliding) enum files
1214
+ for (const [enumName, content] of enumContents) {
1215
+ if (enumCollisionMembers.has(enumName)) continue;
1216
+ enumWritePromises.push(writeFile(join(frameworkDir, `${enumName}.ts`), content));
1217
+ }
1218
+
1219
+ // Wait for all file writes in this framework to complete
1220
+ await Promise.all([...classWritePromises, ...protoWritePromises, ...enumWritePromises]);
1221
+
1222
+ // Emit functions file (typed wrappers for C functions)
1223
+ const fwFuncs = frameworkFunctions.get(framework.name);
1224
+ let hasFunctions = false;
1225
+ if (fwFuncs && fwFuncs.size > 0) {
1226
+ const functionsContent = emitFunctionsFile([...fwFuncs.values()], framework, frameworks, globalClassToFile);
1227
+ await writeFile(join(frameworkDir, "functions.ts"), functionsContent);
1228
+ hasFunctions = true;
1229
+ }
1230
+
1231
+ // Emit framework index
1232
+ const indexContent = emitFrameworkIndex(
1233
+ framework,
1234
+ generatedClasses,
1235
+ generatedProtocols,
1236
+ collisions,
1237
+ generatedIntegerEnums,
1238
+ generatedStringEnums,
1239
+ generatedStringEnumsTypeOnly,
1240
+ enumToFile,
1241
+ hasFunctions
1242
+ );
1243
+ await writeFile(join(frameworkDir, "index.ts"), indexContent);
1244
+ generatedProtocolsByFramework.set(framework.name, generatedProtocols);
1245
+
1246
+ const totalEnumCount =
1247
+ generatedIntegerEnums.length + generatedStringEnums.length + generatedStringEnumsTypeOnly.length;
1248
+ const funcCountStr = hasFunctions ? ` + ${fwFuncs!.size} functions` : "";
1249
+ console.log(
1250
+ ` ${framework.name}: ${generatedClasses.length} class files + ${generatedProtocols.length} protocol files + ${totalEnumCount} enum files${funcCountStr} + index.ts`
1251
+ );
1252
+ }
1253
+
1254
+ // Emit shared files only during full regeneration (they depend on all frameworks)
1255
+ if (!isFiltered) {
1256
+ // Remove old monolithic structs.ts if it exists (replaced by src/structs/ directory)
1257
+ const oldStructsFile = join(SRC_DIR, "structs.ts");
1258
+ if (existsSync(oldStructsFile)) {
1259
+ await unlink(oldStructsFile);
1260
+ }
1261
+
1262
+ // Emit individual struct files under src/structs/
1263
+ const structsDir = join(SRC_DIR, "structs");
1264
+ await mkdir(structsDir, { recursive: true });
1265
+
1266
+ const structWritePromises: Promise<void>[] = [];
1267
+ for (const def of structDefs) {
1268
+ const content = emitStructFile(def, structDefs);
1269
+ structWritePromises.push(writeFile(join(structsDir, `${def.tsName}.ts`), content));
1270
+ }
1271
+ await Promise.all(structWritePromises);
1272
+
1273
+ // Emit structs barrel index
1274
+ const structIndexContent = emitStructIndex(structDefs);
1275
+ await writeFile(join(structsDir, "index.ts"), structIndexContent);
1276
+
1277
+ console.log(` structs: ${structDefs.length} struct files + index.ts`);
1278
+
1279
+ // Emit delegates file (ProtocolMap + createDelegate)
1280
+ const delegatesContent = emitDelegatesFile(frameworks, generatedProtocolsByFramework);
1281
+ await writeFile(join(SRC_DIR, "delegates.ts"), delegatesContent);
1282
+
1283
+ // Emit top-level index
1284
+ const topIndex = emitTopLevelIndex(frameworks.map((f) => f.name));
1285
+ await writeFile(join(SRC_DIR, "index.ts"), topIndex);
1286
+
1287
+ // Copy template files from generator/templates/ into src/
1288
+ const templatesDir = join(import.meta.dir, "templates");
1289
+ if (existsSync(templatesDir)) {
1290
+ const templateFiles = await readdir(templatesDir);
1291
+ await Promise.all(templateFiles.map((file) => copyFile(join(templatesDir, file), join(SRC_DIR, file))));
1292
+ if (templateFiles.length > 0) {
1293
+ console.log(` Copied ${templateFiles.length} template file(s) to src/`);
1294
+ }
1295
+ }
1296
+ }
1297
+
1298
+ const emitTime = ((performance.now() - emitStart) / 1000).toFixed(1);
1299
+ const totalTime = ((performance.now() - globalStart) / 1000).toFixed(1);
1300
+ console.log(`\n Emitted files in ${emitTime}s`);
1301
+ console.log(
1302
+ `\n=== Generation complete${isFiltered ? " (partial)" : ""} (${totalTime}s total, ${discoveryTime}s discovery + ${parseTime}s parsing + ${emitTime}s emit) ===`
1303
+ );
1304
+
1305
+ // Print summary
1306
+ let totalClasses = 0;
1307
+ let totalProtocols = 0;
1308
+ let totalEnums = 0;
1309
+ let totalFunctions = 0;
1310
+ for (const fw of frameworksToProcess) {
1311
+ const dir = join(SRC_DIR, fw.name);
1312
+ const classCount = fw.classes.filter((c) => existsSync(join(dir, `${c}.ts`))).length;
1313
+ const protoCount = fw.protocols.filter((p) => existsSync(join(dir, `${p}.ts`))).length;
1314
+ const enumCount = [...fw.integerEnums, ...fw.stringEnums].filter((e) => existsSync(join(dir, `${e}.ts`))).length;
1315
+ const funcCount = frameworkFunctions.get(fw.name)?.size ?? 0;
1316
+ const funcStr = funcCount > 0 ? `, ${funcCount} functions` : "";
1317
+ console.log(` ${fw.name}: ${classCount} classes, ${protoCount} protocols, ${enumCount} enums${funcStr}`);
1318
+ totalClasses += classCount;
1319
+ totalProtocols += protoCount;
1320
+ totalEnums += enumCount;
1321
+ totalFunctions += funcCount;
1322
+ }
1323
+ console.log(
1324
+ ` Total: ${totalClasses} classes, ${totalProtocols} protocols, ${totalEnums} enums, ${totalFunctions} functions`
1325
+ );
1326
+
1327
+ // Clean up the compiled ObjC helper binary
1328
+ await cleanupResolver();
1329
+ }
1330
+
1331
+ main().catch((err) => {
1332
+ console.error("Generator failed:", err);
1333
+ process.exit(1);
1334
+ });