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.
- package/bin/objcjs-types.ts +31 -0
- package/dist/AVFoundation/functions.d.ts +21 -0
- package/dist/AVFoundation/functions.js +32 -0
- package/dist/AVFoundation/index.d.ts +1 -0
- package/dist/AVFoundation/index.js +1 -0
- package/dist/Accessibility/functions.d.ts +16 -0
- package/dist/Accessibility/functions.js +35 -0
- package/dist/Accessibility/index.d.ts +1 -0
- package/dist/Accessibility/index.js +1 -0
- package/dist/AddressBook/functions.d.ts +98 -0
- package/dist/AddressBook/functions.js +290 -0
- package/dist/AddressBook/index.d.ts +1 -0
- package/dist/AddressBook/index.js +1 -0
- package/dist/AppKit/functions.d.ts +112 -0
- package/dist/AppKit/functions.js +272 -0
- package/dist/AppKit/index.d.ts +1 -0
- package/dist/AppKit/index.js +1 -0
- package/dist/AudioToolbox/functions.d.ts +377 -0
- package/dist/AudioToolbox/functions.js +1124 -0
- package/dist/AudioToolbox/index.d.ts +1 -0
- package/dist/AudioToolbox/index.js +1 -0
- package/dist/AuthenticationServices/functions.d.ts +2 -0
- package/dist/AuthenticationServices/functions.js +5 -0
- package/dist/AuthenticationServices/index.d.ts +1 -0
- package/dist/AuthenticationServices/index.js +1 -0
- package/dist/BrowserEngineCore/functions.d.ts +3 -0
- package/dist/BrowserEngineCore/functions.js +11 -0
- package/dist/BrowserEngineCore/index.d.ts +1 -0
- package/dist/BrowserEngineCore/index.js +1 -0
- package/dist/CoreAudio/functions.d.ts +60 -0
- package/dist/CoreAudio/functions.js +173 -0
- package/dist/CoreAudio/index.d.ts +1 -0
- package/dist/CoreAudio/index.js +1 -0
- package/dist/CoreMIDI/functions.d.ts +96 -0
- package/dist/CoreMIDI/functions.js +287 -0
- package/dist/CoreMIDI/index.d.ts +1 -0
- package/dist/CoreMIDI/index.js +1 -0
- package/dist/CoreML/functions.d.ts +2 -0
- package/dist/CoreML/functions.js +5 -0
- package/dist/CoreML/index.d.ts +1 -0
- package/dist/CoreML/index.js +1 -0
- package/dist/CoreMediaIO/functions.d.ts +38 -0
- package/dist/CoreMediaIO/functions.js +107 -0
- package/dist/CoreMediaIO/index.d.ts +1 -0
- package/dist/CoreMediaIO/index.js +1 -0
- package/dist/CoreText/functions.d.ts +209 -0
- package/dist/CoreText/functions.js +611 -0
- package/dist/CoreText/index.d.ts +1 -0
- package/dist/CoreText/index.js +1 -0
- package/dist/CoreWLAN/functions.d.ts +23 -0
- package/dist/CoreWLAN/functions.js +56 -0
- package/dist/CoreWLAN/index.d.ts +1 -0
- package/dist/CoreWLAN/index.js +1 -0
- package/dist/DeviceDiscoveryExtension/functions.d.ts +11 -0
- package/dist/DeviceDiscoveryExtension/functions.js +17 -0
- package/dist/DeviceDiscoveryExtension/index.d.ts +1 -0
- package/dist/DeviceDiscoveryExtension/index.js +1 -0
- package/dist/DiscRecording/functions.d.ts +97 -0
- package/dist/DiscRecording/functions.js +290 -0
- package/dist/DiscRecording/index.d.ts +1 -0
- package/dist/DiscRecording/index.js +1 -0
- package/dist/DiscRecordingUI/functions.d.ts +13 -0
- package/dist/DiscRecordingUI/functions.js +38 -0
- package/dist/DiscRecordingUI/index.d.ts +1 -0
- package/dist/DiscRecordingUI/index.js +1 -0
- package/dist/ExceptionHandling/functions.d.ts +1 -0
- package/dist/ExceptionHandling/functions.js +5 -0
- package/dist/ExceptionHandling/index.d.ts +1 -0
- package/dist/ExceptionHandling/index.js +1 -0
- package/dist/FSKit/functions.d.ts +4 -0
- package/dist/FSKit/functions.js +11 -0
- package/dist/FSKit/index.d.ts +1 -0
- package/dist/FSKit/index.js +1 -0
- package/dist/Foundation/functions.d.ts +145 -0
- package/dist/Foundation/functions.js +386 -0
- package/dist/Foundation/index.d.ts +1 -0
- package/dist/Foundation/index.js +1 -0
- package/dist/GLKit/functions.d.ts +51 -0
- package/dist/GLKit/functions.js +146 -0
- package/dist/GLKit/index.d.ts +1 -0
- package/dist/GLKit/index.js +1 -0
- package/dist/GameController/functions.d.ts +18 -0
- package/dist/GameController/functions.js +44 -0
- package/dist/GameController/index.d.ts +1 -0
- package/dist/GameController/index.js +1 -0
- package/dist/HealthKit/functions.d.ts +19 -0
- package/dist/HealthKit/functions.js +35 -0
- package/dist/HealthKit/index.d.ts +1 -0
- package/dist/HealthKit/index.js +1 -0
- package/dist/IOSurface/functions.d.ts +53 -0
- package/dist/IOSurface/functions.js +155 -0
- package/dist/IOSurface/index.d.ts +1 -0
- package/dist/IOSurface/index.js +1 -0
- package/dist/IOUSBHost/functions.d.ts +44 -0
- package/dist/IOUSBHost/functions.js +131 -0
- package/dist/IOUSBHost/index.d.ts +1 -0
- package/dist/IOUSBHost/index.js +1 -0
- package/dist/InstantMessage/functions.d.ts +1 -0
- package/dist/InstantMessage/functions.js +5 -0
- package/dist/InstantMessage/index.d.ts +1 -0
- package/dist/InstantMessage/index.js +1 -0
- package/dist/JavaRuntimeSupport/functions.d.ts +40 -0
- package/dist/JavaRuntimeSupport/functions.js +113 -0
- package/dist/JavaRuntimeSupport/index.d.ts +1 -0
- package/dist/JavaRuntimeSupport/index.js +1 -0
- package/dist/JavaScriptCore/functions.d.ts +120 -0
- package/dist/JavaScriptCore/functions.js +359 -0
- package/dist/JavaScriptCore/index.d.ts +1 -0
- package/dist/JavaScriptCore/index.js +1 -0
- package/dist/MLCompute/functions.d.ts +27 -0
- package/dist/MLCompute/functions.js +41 -0
- package/dist/MLCompute/index.d.ts +1 -0
- package/dist/MLCompute/index.js +1 -0
- package/dist/MapKit/functions.d.ts +23 -0
- package/dist/MapKit/functions.js +56 -0
- package/dist/MapKit/index.d.ts +1 -0
- package/dist/MapKit/index.js +1 -0
- package/dist/Matter/functions.d.ts +17 -0
- package/dist/Matter/functions.js +26 -0
- package/dist/Matter/index.d.ts +1 -0
- package/dist/Matter/index.js +1 -0
- package/dist/MediaAccessibility/functions.d.ts +28 -0
- package/dist/MediaAccessibility/functions.js +83 -0
- package/dist/MediaAccessibility/index.d.ts +1 -0
- package/dist/MediaAccessibility/index.js +1 -0
- package/dist/MediaPlayer/functions.d.ts +3 -0
- package/dist/MediaPlayer/functions.js +11 -0
- package/dist/MediaPlayer/index.d.ts +1 -0
- package/dist/MediaPlayer/index.js +1 -0
- package/dist/Metal/functions.d.ts +14 -0
- package/dist/Metal/functions.js +26 -0
- package/dist/Metal/index.d.ts +1 -0
- package/dist/Metal/index.js +1 -0
- package/dist/MetalKit/functions.d.ts +11 -0
- package/dist/MetalKit/functions.js +20 -0
- package/dist/MetalKit/index.d.ts +1 -0
- package/dist/MetalKit/index.js +1 -0
- package/dist/MetalPerformanceShaders/functions.d.ts +7 -0
- package/dist/MetalPerformanceShaders/functions.js +14 -0
- package/dist/MetalPerformanceShaders/index.d.ts +1 -0
- package/dist/MetalPerformanceShaders/index.js +1 -0
- package/dist/NearbyInteraction/functions.d.ts +3 -0
- package/dist/NearbyInteraction/functions.js +5 -0
- package/dist/NearbyInteraction/index.d.ts +1 -0
- package/dist/NearbyInteraction/index.js +1 -0
- package/dist/ParavirtualizedGraphics/functions.d.ts +7 -0
- package/dist/ParavirtualizedGraphics/functions.js +14 -0
- package/dist/ParavirtualizedGraphics/index.d.ts +1 -0
- package/dist/ParavirtualizedGraphics/index.js +1 -0
- package/dist/QuartzCore/functions.d.ts +19 -0
- package/dist/QuartzCore/functions.js +50 -0
- package/dist/QuartzCore/index.d.ts +1 -0
- package/dist/QuartzCore/index.js +1 -0
- package/dist/SceneKit/functions.d.ts +17 -0
- package/dist/SceneKit/functions.js +38 -0
- package/dist/SceneKit/index.d.ts +1 -0
- package/dist/SceneKit/index.js +1 -0
- package/dist/SensorKit/functions.d.ts +4 -0
- package/dist/SensorKit/functions.js +14 -0
- package/dist/SensorKit/index.d.ts +1 -0
- package/dist/SensorKit/index.js +1 -0
- package/dist/ServiceManagement/functions.d.ts +7 -0
- package/dist/ServiceManagement/functions.js +20 -0
- package/dist/ServiceManagement/index.d.ts +1 -0
- package/dist/ServiceManagement/index.js +1 -0
- package/dist/StoreKit/functions.d.ts +1 -0
- package/dist/StoreKit/functions.js +5 -0
- package/dist/StoreKit/index.d.ts +1 -0
- package/dist/StoreKit/index.js +1 -0
- package/dist/VideoToolbox/functions.d.ts +81 -0
- package/dist/VideoToolbox/functions.js +236 -0
- package/dist/VideoToolbox/index.d.ts +1 -0
- package/dist/VideoToolbox/index.js +1 -0
- package/dist/Vision/functions.d.ts +16 -0
- package/dist/Vision/functions.js +38 -0
- package/dist/Vision/index.d.ts +1 -0
- package/dist/Vision/index.js +1 -0
- package/generator/ast-parser.ts +1368 -0
- package/generator/clang.ts +167 -0
- package/generator/custom.ts +936 -0
- package/generator/discover.ts +111 -0
- package/generator/emitter.ts +2026 -0
- package/generator/frameworks.ts +135 -0
- package/generator/index.ts +1334 -0
- package/generator/parse-worker.ts +263 -0
- package/generator/resolve-strings.ts +121 -0
- package/generator/struct-fields.ts +46 -0
- package/generator/templates/bind.ts +100 -0
- package/generator/templates/helpers.ts +70 -0
- package/generator/templates/nsdata.ts +97 -0
- package/generator/templates/osversion.ts +91 -0
- package/generator/type-mapper.ts +615 -0
- package/generator/worker-pool.ts +309 -0
- 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
|
+
});
|