@react-native-native/nativ-fabric 0.1.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 (42) hide show
  1. package/NativFabric.podspec +41 -0
  2. package/android/build.gradle +128 -0
  3. package/android/src/main/AndroidManifest.xml +2 -0
  4. package/android/src/main/cpp/CMakeLists.txt +59 -0
  5. package/android/src/main/cpp/NativBindingsInstaller.cpp +393 -0
  6. package/android/src/main/cpp/NativRuntime.cpp +508 -0
  7. package/android/src/main/java/com/nativfabric/ComposeHost.kt +26 -0
  8. package/android/src/main/java/com/nativfabric/NativContainerPackage.kt +35 -0
  9. package/android/src/main/java/com/nativfabric/NativContainerView.kt +51 -0
  10. package/android/src/main/java/com/nativfabric/NativContainerViewManager.kt +62 -0
  11. package/android/src/main/java/com/nativfabric/NativRuntime.kt +201 -0
  12. package/android/src/main/java/com/nativfabric/NativRuntimeModule.kt +37 -0
  13. package/android/src/main/java/com/nativfabric/compose/ComposeWrappers.kt +45 -0
  14. package/app.plugin.js +159 -0
  15. package/expo-module.config.json +6 -0
  16. package/ios/NativContainerComponentView.mm +137 -0
  17. package/ios/NativRuntime.h +36 -0
  18. package/ios/NativRuntime.mm +549 -0
  19. package/metro/Nativ.h +126 -0
  20. package/metro/compilers/android-compiler.js +339 -0
  21. package/metro/compilers/dylib-compiler.js +474 -0
  22. package/metro/compilers/kotlin-compiler.js +632 -0
  23. package/metro/compilers/rust-compiler.js +722 -0
  24. package/metro/compilers/static-compiler.js +1118 -0
  25. package/metro/compilers/swift-compiler.js +363 -0
  26. package/metro/extractors/cpp-ast-extractor.js +126 -0
  27. package/metro/extractors/kotlin-extractor.js +125 -0
  28. package/metro/extractors/rust-extractor.js +118 -0
  29. package/metro/index.js +236 -0
  30. package/metro/transformer.js +649 -0
  31. package/metro/utils/bridge-generator.js +50 -0
  32. package/metro/utils/compile-commands.js +104 -0
  33. package/metro/utils/cpp-daemon.js +344 -0
  34. package/metro/utils/dts-generator.js +32 -0
  35. package/metro/utils/include-resolver.js +73 -0
  36. package/metro/utils/kotlin-daemon.js +394 -0
  37. package/metro/utils/type-mapper.js +63 -0
  38. package/package.json +43 -0
  39. package/react-native.config.js +13 -0
  40. package/src/NativContainerNativeComponent.ts +9 -0
  41. package/src/NativeNativRuntime.ts +8 -0
  42. package/src/index.ts +4 -0
@@ -0,0 +1,363 @@
1
+ /**
2
+ * Compiles a .swift file to a signed arm64 iOS dylib.
3
+ * Same pattern as dylib-compiler.js for C++ and rust-compiler.js for Rust.
4
+ */
5
+
6
+ const { execSync } = require('child_process');
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+
10
+ let _sdkPaths = {};
11
+ let _signingIdentity = null;
12
+ let _resolved = false;
13
+
14
+ function getSdkPath(target) {
15
+ if (_sdkPaths[target]) return _sdkPaths[target];
16
+ const sdk = target === 'simulator' ? 'iphonesimulator' : 'iphoneos';
17
+ try {
18
+ _sdkPaths[target] = execSync(`xcrun --sdk ${sdk} --show-sdk-path`, {
19
+ encoding: 'utf8',
20
+ }).trim();
21
+ } catch {}
22
+ return _sdkPaths[target] || null;
23
+ }
24
+
25
+ function resolveOnce(projectRoot) {
26
+ if (_resolved) return;
27
+ _resolved = true;
28
+
29
+ try {
30
+ let appTeamId = null;
31
+ try {
32
+ const appJson = JSON.parse(fs.readFileSync(path.join(projectRoot, 'app.json'), 'utf8'));
33
+ appTeamId = appJson?.expo?.ios?.appleTeamId || null;
34
+ } catch {}
35
+
36
+ if (!appTeamId) {
37
+ try {
38
+ const pbx = execSync(
39
+ `find "${projectRoot}/ios" -name "project.pbxproj" -maxdepth 3 2>/dev/null`,
40
+ { encoding: 'utf8' }
41
+ ).trim().split('\n')[0];
42
+ if (pbx) {
43
+ const m = fs.readFileSync(pbx, 'utf8').match(/DEVELOPMENT_TEAM\s*=\s*(\w+)/);
44
+ if (m) appTeamId = m[1];
45
+ }
46
+ } catch {}
47
+ }
48
+
49
+ if (appTeamId) {
50
+ const identities = execSync('security find-identity -v -p codesigning', {
51
+ encoding: 'utf8',
52
+ });
53
+ const entries = [...identities.matchAll(/([A-F0-9]{40})\s+"([^"]+)"/g)];
54
+ for (const [, , name] of entries) {
55
+ try {
56
+ const subject = execSync(
57
+ `security find-certificate -c "${name}" -p 2>/dev/null | openssl x509 -noout -subject 2>/dev/null`,
58
+ { encoding: 'utf8' }
59
+ );
60
+ if (subject.includes(`OU=${appTeamId}`)) {
61
+ _signingIdentity = name;
62
+ break;
63
+ }
64
+ } catch {}
65
+ }
66
+ }
67
+ } catch {}
68
+ }
69
+
70
+ /**
71
+ * Extract Swift function exports: @_cdecl functions with nativ_ prefix.
72
+ */
73
+ function extractSwiftExports(filepath) {
74
+ let src;
75
+ try { src = fs.readFileSync(filepath, 'utf8'); } catch { return []; }
76
+
77
+ const moduleId = path.basename(filepath, '.swift').toLowerCase();
78
+ const exports = [];
79
+
80
+ // Match: // @nativ_export or // @nativ_export(sync) or // @nativ_export(sync, main)
81
+ const pattern = /\/\/\s*@?nativ_export(?:\s*\(\s*([^)]*)\s*\))?\s*\n\s*(?:@_cdecl\s*\([^)]*\)\s*\n\s*)?func\s+(\w+)\s*\(([^)]*)\)\s*(?:->\s*(\S+))?\s*\{/g;
82
+ let match;
83
+ while ((match = pattern.exec(src)) !== null) {
84
+ const [, modeStr, name, argsStr, retType] = match;
85
+ const flags = (modeStr || '').split(',').map(s => s.trim());
86
+ const cdeclName = `nativ_swift_${moduleId}_${name}`;
87
+
88
+ // Parse args (skip _ labels and UnsafePointer types from old-style exports)
89
+ const args = argsStr.trim()
90
+ ? argsStr.split(',').map(a => {
91
+ const m = a.trim().match(/(?:\w+\s+)?(\w+)\s*:\s*(.+)/);
92
+ return m ? { name: m[1], type: m[2].trim() } : null;
93
+ }).filter(Boolean).filter(a => !a.type.includes('UnsafePointer'))
94
+ : [];
95
+
96
+ exports.push({
97
+ name,
98
+ async: flags.includes('async'),
99
+ mainThread: flags.includes('main'),
100
+ cdeclName,
101
+ args,
102
+ ret: retType || 'Void',
103
+ });
104
+ }
105
+
106
+ return exports;
107
+ }
108
+
109
+ function compileSwiftDylib(filepath, projectRoot, { target = 'device' } = {}) {
110
+ resolveOnce(projectRoot);
111
+ const sdkPath = getSdkPath(target);
112
+ if (!sdkPath) return null;
113
+
114
+ const targetTriple = target === 'simulator'
115
+ ? 'arm64-apple-ios15.1-simulator'
116
+ : 'arm64-apple-ios15.1';
117
+
118
+ const name = path.basename(filepath, '.swift');
119
+ const moduleId = name.toLowerCase();
120
+ const outputDir = path.join(projectRoot, '.nativ/dylibs', target);
121
+ fs.mkdirSync(outputDir, { recursive: true });
122
+ const _isComp = (() => {
123
+ try { return fs.readFileSync(filepath, 'utf8').includes('@nativ_component'); } catch { return false; }
124
+ })();
125
+ const dylibName = _isComp ? `nativ_${moduleId}` : moduleId;
126
+ const dylibPath = path.join(outputDir, `${dylibName}.dylib`);
127
+
128
+ // Generate Swift bridge with @_cdecl wrappers + C registration file
129
+ const exports = extractSwiftExports(filepath);
130
+
131
+ // Check if this is a component (has @nativ_component or nativ::component)
132
+ const userSrc = fs.readFileSync(filepath, 'utf8');
133
+ const isComponent = userSrc.includes('@nativ_component') || userSrc.includes('nativ::component');
134
+
135
+ if (isComponent) {
136
+ // Parse the SwiftUI View struct to extract props
137
+ const structMatch = userSrc.match(/\/\/\s*@nativ_component\s*\n\s*struct\s+(\w+)\s*:\s*View\s*\{([\s\S]*?)var\s+body\s*:\s*some\s+View/);
138
+ const structName = structMatch ? structMatch[1] : name;
139
+ const propsBlock = structMatch ? structMatch[2] : '';
140
+
141
+ // Parse fields: let title: String, let count: Int, var opacity: Double = 1.0
142
+ const props = [];
143
+ for (const line of propsBlock.split('\n')) {
144
+ const m = line.trim().match(/(?:let|var)\s+(\w+)\s*:\s*(\w+)/);
145
+ if (m) {
146
+ const [, propName, propType] = m;
147
+ if (!['body'].includes(propName)) {
148
+ props.push({ name: propName, type: propType });
149
+ }
150
+ }
151
+ }
152
+
153
+ // Generate Swift bridge file with render function + JSI prop extraction
154
+ const swiftBridgePath = path.join(outputDir, `${moduleId}_bridge.swift`);
155
+ const renderFnName = `nativ_${moduleId}_render`;
156
+
157
+ const propExtractions = props.map(p => {
158
+ if (p.type === 'String') {
159
+ return ` let ${p.name} = String(cString: nativ_jsi_get_string(runtime, props, "${p.name}"))`;
160
+ } else if (p.type === 'Double' || p.type === 'Float' || p.type === 'CGFloat') {
161
+ return ` let ${p.name} = ${p.type}(nativ_jsi_get_number(runtime, props, "${p.name}"))`;
162
+ } else if (p.type === 'Int') {
163
+ return ` let ${p.name} = Int(nativ_jsi_get_number(runtime, props, "${p.name}"))`;
164
+ } else if (p.type === 'Bool') {
165
+ return ` let ${p.name} = nativ_jsi_has_prop(runtime, props, "${p.name}") != 0 && nativ_jsi_get_number(runtime, props, "${p.name}") != 0`;
166
+ } else if (p.type === 'Color') {
167
+ return ` let ${p.name}: Color = {
168
+ let hex = String(cString: nativ_jsi_get_string(runtime, props, "${p.name}"))
169
+ let scanner = Scanner(string: hex.hasPrefix("#") ? String(hex.dropFirst()) : hex)
170
+ var rgb: UInt64 = 0; scanner.scanHexInt64(&rgb)
171
+ return Color(red: Double((rgb >> 16) & 0xFF) / 255, green: Double((rgb >> 8) & 0xFF) / 255, blue: Double(rgb & 0xFF) / 255)
172
+ }()`;
173
+ } else {
174
+ return ` // TODO: unsupported prop type ${p.type} for ${p.name}`;
175
+ }
176
+ });
177
+
178
+ // Build the struct initializer args
179
+ const initArgs = props.map(p => {
180
+ if (p.type === 'Color') {
181
+ return `${p.name}: Color(red: nativ_jsi_get_number(runtime, props, "r"), green: nativ_jsi_get_number(runtime, props, "g"), blue: nativ_jsi_get_number(runtime, props, "b"))`;
182
+ }
183
+ return `${p.name}: ${p.name}`;
184
+ }).join(', ');
185
+
186
+ const bridgeSwift = `import SwiftUI
187
+ import UIKit
188
+
189
+ // Auto-generated bridge for ${structName}
190
+
191
+ @_cdecl("${renderFnName}")
192
+ func ${renderFnName}(
193
+ _ view: UnsafeMutableRawPointer,
194
+ _ width: Float, _ height: Float,
195
+ _ runtime: UnsafeMutableRawPointer?,
196
+ _ props: UnsafeMutableRawPointer?
197
+ ) {
198
+ let parentView = Unmanaged<UIView>.fromOpaque(view).takeUnretainedValue()
199
+
200
+ // Extract props via JSI C API
201
+ ${propExtractions.join('\n')}
202
+
203
+ let swiftUIView = ${structName}(${initArgs})
204
+ let hostingController = UIHostingController(rootView: swiftUIView)
205
+ hostingController.view.frame = CGRect(x: 0, y: 0, width: CGFloat(width), height: CGFloat(height))
206
+ hostingController.view.backgroundColor = .clear
207
+
208
+ objc_setAssociatedObject(parentView, "nativHosting", hostingController, .OBJC_ASSOCIATION_RETAIN)
209
+ parentView.addSubview(hostingController.view)
210
+ }
211
+
212
+ // JSI C API — resolved at dlopen time via -undefined dynamic_lookup
213
+ @_silgen_name("nativ_jsi_get_string")
214
+ func nativ_jsi_get_string(_ rt: UnsafeMutableRawPointer?, _ obj: UnsafeMutableRawPointer?, _ name: UnsafePointer<CChar>) -> UnsafePointer<CChar>
215
+
216
+ @_silgen_name("nativ_jsi_get_number")
217
+ func nativ_jsi_get_number(_ rt: UnsafeMutableRawPointer?, _ obj: UnsafeMutableRawPointer?, _ name: UnsafePointer<CChar>) -> Double
218
+
219
+ @_silgen_name("nativ_jsi_has_prop")
220
+ func nativ_jsi_has_prop(_ rt: UnsafeMutableRawPointer?, _ obj: UnsafeMutableRawPointer?, _ name: UnsafePointer<CChar>) -> Int32
221
+ `;
222
+ fs.writeFileSync(swiftBridgePath, bridgeSwift);
223
+
224
+ // C registration file
225
+ const cBridgePath = path.join(outputDir, `${moduleId}_reg.c`);
226
+ fs.writeFileSync(cBridgePath, `
227
+ typedef void (*NativRenderFn)(void*, float, float, void*, void*);
228
+ extern void nativ_register_render(const char*, NativRenderFn);
229
+ extern void ${renderFnName}(void*, float, float, void*, void*);
230
+
231
+ __attribute__((constructor))
232
+ void nativ_register_${moduleId}(void) {
233
+ nativ_register_render("nativ.${moduleId}", ${renderFnName});
234
+ }
235
+ `);
236
+
237
+ const cmd = [
238
+ 'swiftc',
239
+ '-emit-library',
240
+ '-target', targetTriple,
241
+ '-sdk', sdkPath,
242
+ '-Xlinker', '-undefined',
243
+ '-Xlinker', 'dynamic_lookup',
244
+ '-o', dylibPath,
245
+ filepath,
246
+ swiftBridgePath,
247
+ cBridgePath,
248
+ ];
249
+
250
+ console.log(`[nativ] Compiling ${name}.swift component via swiftc...`);
251
+ try {
252
+ execSync(cmd.join(' '), { stdio: 'pipe', encoding: 'utf8' });
253
+ } catch (err) {
254
+ console.error(`[nativ] Swift compile failed: ${name}.swift`);
255
+ console.error((err.stderr || '').slice(0, 2000));
256
+ return null;
257
+ }
258
+
259
+ if (_signingIdentity) {
260
+ try { execSync(`codesign -fs "${_signingIdentity}" "${dylibPath}"`, { stdio: 'pipe' }); } catch {}
261
+ }
262
+
263
+ const size = fs.statSync(dylibPath).size;
264
+ console.log(`[nativ] Built ${moduleId}.dylib component (${(size / 1024).toFixed(1)}KB)`);
265
+ return { dylibPath, exports: [], isComponent: true };
266
+ }
267
+
268
+ // Function exports: generate Swift + C bridges
269
+ const swiftBridgePath = path.join(outputDir, `${moduleId}_bridge.swift`);
270
+ let swiftWrappers = 'import Foundation\n';
271
+ swiftWrappers += exports.map(fn => {
272
+ const retType = fn.ret || 'Void';
273
+ const argPassthrough = fn.args.map(a => a.name).join(', ');
274
+
275
+ let resultExpr;
276
+ if (retType === 'String') {
277
+ resultExpr = `UnsafePointer(strdup("\\"" + result + "\\"")!)`;
278
+ } else if (retType === 'Bool') {
279
+ resultExpr = `UnsafePointer(strdup(result ? "true" : "false")!)`;
280
+ } else if (retType === 'Void') {
281
+ resultExpr = `UnsafePointer(strdup("null")!)`;
282
+ } else {
283
+ resultExpr = `UnsafePointer(strdup(String(result))!)`;
284
+ }
285
+
286
+ if (fn.mainThread) {
287
+ return `
288
+ @_cdecl("${fn.cdeclName}")
289
+ func _nativ_${fn.name}(_ argsJson: UnsafePointer<CChar>) -> UnsafePointer<CChar> {
290
+ var ptr: UnsafePointer<CChar>!
291
+ DispatchQueue.main.sync {
292
+ let result = ${fn.name}(${argPassthrough})
293
+ ptr = ${resultExpr}
294
+ }
295
+ return ptr
296
+ }`;
297
+ }
298
+
299
+ return `
300
+ @_cdecl("${fn.cdeclName}")
301
+ func _nativ_${fn.name}(_ argsJson: UnsafePointer<CChar>) -> UnsafePointer<CChar> {
302
+ let result = ${fn.name}(${argPassthrough})
303
+ return ${resultExpr}
304
+ }`;
305
+ }).join('\n');
306
+
307
+ fs.writeFileSync(swiftBridgePath, swiftWrappers.toString());
308
+
309
+ // C registration file
310
+ const cBridgePath = path.join(outputDir, `${moduleId}_reg.c`);
311
+ const registrations = exports.map(fn =>
312
+ ` nativ_register_sync("${moduleId}", "${fn.name}", ${fn.cdeclName});`
313
+ ).join('\n');
314
+ const declarations = exports.map(fn =>
315
+ `extern const char* ${fn.cdeclName}(const char*);`
316
+ ).join('\n');
317
+
318
+ fs.writeFileSync(cBridgePath, `
319
+ typedef const char* (*NativSyncFn)(const char*);
320
+ extern void nativ_register_sync(const char*, const char*, NativSyncFn);
321
+ ${declarations}
322
+
323
+ __attribute__((constructor))
324
+ void nativ_register_${moduleId}(void) {
325
+ ${registrations}
326
+ }
327
+ `);
328
+
329
+ const cmd = [
330
+ 'swiftc',
331
+ '-emit-library',
332
+ '-target', targetTriple,
333
+ '-sdk', sdkPath,
334
+ '-Xlinker', '-undefined',
335
+ '-Xlinker', 'dynamic_lookup',
336
+ '-o', dylibPath,
337
+ filepath,
338
+ swiftBridgePath,
339
+ cBridgePath,
340
+ ];
341
+
342
+ console.log(`[nativ] Compiling ${name}.swift via swiftc...`);
343
+ try {
344
+ execSync(cmd.join(' '), { stdio: 'pipe', encoding: 'utf8' });
345
+ } catch (err) {
346
+ console.error(`[nativ] Swift compile failed: ${name}.swift`);
347
+ console.error((err.stderr || '').slice(0, 2000));
348
+ return null;
349
+ }
350
+
351
+ // Sign
352
+ if (_signingIdentity) {
353
+ try {
354
+ execSync(`codesign -fs "${_signingIdentity}" "${dylibPath}"`, { stdio: 'pipe' });
355
+ } catch {}
356
+ }
357
+
358
+ const size = fs.statSync(dylibPath).size;
359
+ console.log(`[nativ] Built ${moduleId}.dylib (${(size / 1024).toFixed(1)}KB)`);
360
+ return { dylibPath, exports };
361
+ }
362
+
363
+ module.exports = { compileSwiftDylib, extractSwiftExports };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Extracts NATIV_EXPORT annotated functions from C++/ObjC++ files.
3
+ *
4
+ * Uses regex to find NATIV_EXPORT annotations and parse function signatures.
5
+ * This is simpler and more portable than relying on clang's JSON AST dump
6
+ * (which doesn't include annotation values in all versions).
7
+ */
8
+
9
+ const fs = require('fs');
10
+
11
+ /**
12
+ * Parse a C++/ObjC++ file and return all NATIV_EXPORT-annotated function declarations.
13
+ *
14
+ * @param {string} filename — absolute path to .cpp/.mm file
15
+ * @param {string[]} _includePaths — unused (kept for API compat)
16
+ * @returns {{ name: string, async: boolean, args: { name: string, type: string }[], ret: string }[]}
17
+ */
18
+ function isCppComponent(filename) {
19
+ try {
20
+ const src = fs.readFileSync(filename, 'utf8');
21
+ return src.includes('nativ::component') || src.includes('NATIV_COMPONENT') || src.includes('NATIV_COMPONENT');
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ function extractCppExports(filename, _includePaths) {
28
+ let src;
29
+ try {
30
+ src = fs.readFileSync(filename, 'utf8');
31
+ } catch {
32
+ return [];
33
+ }
34
+
35
+ const exports = [];
36
+
37
+ // Match: NATIV_EXPORT(sync|async[, main])\n<return_type> <name>(<args>)
38
+ // Supports: NATIV_EXPORT(sync), NATIV_EXPORT(async), NATIV_EXPORT(sync, main)
39
+ const pattern = /NATIV_EXPORT\s*\(\s*([^)]+)\s*\)\s*\n\s*(.+?)\s+(\w+)\s*\(([^)]*)\)/g;
40
+
41
+ let match;
42
+ while ((match = pattern.exec(src)) !== null) {
43
+ const [, modeStr, retType, name, argsStr] = match;
44
+ const flags = modeStr.split(',').map(s => s.trim());
45
+ const mode = flags[0]; // 'sync' or 'async'
46
+ const mainThread = flags.includes('main');
47
+
48
+ const args = argsStr
49
+ .split(',')
50
+ .map(a => a.trim())
51
+ .filter(Boolean)
52
+ .map(arg => {
53
+ // Parse "const std::string& name" → { type: "const std::string&", name: "name" }
54
+ const parts = arg.match(/^(.+?)\s+(\w+)$/);
55
+ if (parts) {
56
+ return { type: parts[1].trim(), name: parts[2] };
57
+ }
58
+ return { type: arg, name: '_unnamed' };
59
+ });
60
+
61
+ exports.push({
62
+ name,
63
+ async: mode === 'async',
64
+ mainThread,
65
+ args,
66
+ ret: retType.trim(),
67
+ });
68
+ }
69
+
70
+ return exports;
71
+ }
72
+
73
+ /**
74
+ * Extract component props from a C++ props struct.
75
+ * Parses: struct XxxProps { std::string title = "default"; double opacity = 1.0; ... };
76
+ */
77
+ function extractCppComponentProps(filename) {
78
+ let src;
79
+ try { src = fs.readFileSync(filename, 'utf8'); } catch { return []; }
80
+
81
+ // Find NATIV_COMPONENT(name, PropsType)
82
+ const compMatch = src.match(/NATIV_COMPONENT\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)/);
83
+ if (!compMatch) return [];
84
+
85
+ const propsTypeName = compMatch[2];
86
+
87
+ // Find the struct definition
88
+ const structRegex = new RegExp(`struct\\s+${propsTypeName}\\s*\\{([^}]*)\\}`, 's');
89
+ const structMatch = src.match(structRegex);
90
+ if (!structMatch) return [];
91
+
92
+ const props = [];
93
+ const CPP_TO_TS = {
94
+ 'std::string': 'string',
95
+ 'double': 'number',
96
+ 'float': 'number',
97
+ 'int': 'number',
98
+ 'int32_t': 'number',
99
+ 'bool': 'boolean',
100
+ 'std::function<void()>': '(() => void)',
101
+ 'std::function<void(std::string)>': '((arg: string) => void)',
102
+ };
103
+
104
+ for (const line of structMatch[1].split('\n')) {
105
+ const trimmed = line.trim().replace(/;$/, '').trim();
106
+ if (!trimmed || trimmed.startsWith('//')) continue;
107
+
108
+ // Match: type name = default or type name
109
+ const fieldMatch = trimmed.match(/^(.+?)\s+(\w+)(?:\s*=\s*(.+))?$/);
110
+ if (!fieldMatch) continue;
111
+
112
+ const [, cppType, name, defaultVal] = fieldMatch;
113
+ const cleanType = cppType.trim();
114
+
115
+ // snake_case → camelCase
116
+ const jsName = name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
117
+
118
+ const tsType = CPP_TO_TS[cleanType] || 'unknown';
119
+
120
+ props.push({ name, jsName, cppType: cleanType, tsType, defaultVal: defaultVal?.trim() });
121
+ }
122
+
123
+ return props;
124
+ }
125
+
126
+ module.exports = { extractCppExports, isCppComponent, extractCppComponentProps };
@@ -0,0 +1,125 @@
1
+ /**
2
+ * kotlin-extractor.js — extracts @nativ_export functions and @nativ_component
3
+ * annotations from Kotlin source files.
4
+ *
5
+ * Mirrors rust-extractor.js / cpp-ast-extractor.js for the Kotlin path.
6
+ */
7
+
8
+ const fs = require('fs');
9
+
10
+ // Kotlin type → TypeScript type mapping
11
+ const typeMap = {
12
+ 'Int': 'number',
13
+ 'Long': 'number',
14
+ 'Float': 'number',
15
+ 'Double': 'number',
16
+ 'Boolean': 'boolean',
17
+ 'String': 'string',
18
+ 'Unit': 'void',
19
+ };
20
+
21
+ function tsType(ktType) {
22
+ return typeMap[ktType] || 'any';
23
+ }
24
+
25
+ /**
26
+ * Extract exported functions and component info from a .kt file.
27
+ *
28
+ * Annotations (in comments, like Swift):
29
+ * // @nativ_export(sync)
30
+ * fun fibonacci(n: Int): Int { ... }
31
+ *
32
+ * // @nativ_component
33
+ * @Composable
34
+ * fun Counter(count: Int, onPress: () -> Unit) { ... }
35
+ */
36
+ function extractKotlinExports(filepath) {
37
+ const src = fs.readFileSync(filepath, 'utf8');
38
+ const lines = src.split('\n');
39
+ const functions = [];
40
+ let isComponent = false;
41
+ const componentProps = [];
42
+
43
+ for (let i = 0; i < lines.length; i++) {
44
+ const line = lines[i].trim();
45
+
46
+ // Check for // @nativ_export or // @nativ_component
47
+ const exportMatch = line.match(/\/\/\s*@nativ_export\s*\(?\s*(sync|async)?\s*\)?/);
48
+ const componentMatch = line.match(/\/\/\s*@nativ_component/);
49
+
50
+ if (exportMatch) {
51
+ const isAsync = exportMatch[1] === 'async';
52
+ // Find the next `fun` declaration
53
+ for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
54
+ const fnLine = lines[j].trim();
55
+ const fnMatch = fnLine.match(/fun\s+(\w+)\s*\(([^)]*)\)\s*(?::\s*(\S+))?/);
56
+ if (fnMatch) {
57
+ const name = fnMatch[1];
58
+ const argsStr = fnMatch[2].trim();
59
+ const ret = fnMatch[3] || 'Unit';
60
+
61
+ const args = [];
62
+ if (argsStr) {
63
+ // Parse "name: Type, name2: Type2"
64
+ for (const part of argsStr.split(',')) {
65
+ const argMatch = part.trim().match(/(\w+)\s*:\s*(\S+)/);
66
+ if (argMatch) {
67
+ args.push({
68
+ name: argMatch[1],
69
+ type: argMatch[2],
70
+ tsType: tsType(argMatch[2]),
71
+ });
72
+ }
73
+ }
74
+ }
75
+
76
+ functions.push({
77
+ name,
78
+ args,
79
+ ret,
80
+ tsType: tsType(ret),
81
+ async: isAsync,
82
+ });
83
+ i = j;
84
+ break;
85
+ }
86
+ }
87
+ }
88
+
89
+ if (componentMatch) {
90
+ isComponent = true;
91
+ // Find the next @Composable fun
92
+ for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
93
+ const fnLine = lines[j].trim();
94
+ const fnMatch = fnLine.match(/fun\s+\w+\s*\(([^)]*)\)/);
95
+ if (fnMatch) {
96
+ const argsStr = fnMatch[1].trim();
97
+ if (argsStr) {
98
+ for (const part of argsStr.split(',')) {
99
+ const argMatch = part.trim().match(/(\w+)\s*:\s*(.+)/);
100
+ if (argMatch) {
101
+ const pName = argMatch[1];
102
+ const pType = argMatch[2].trim();
103
+ // Skip callback types for props (they stay as callbacks)
104
+ const isCallback = pType.includes('->');
105
+ componentProps.push({
106
+ name: pName,
107
+ jsName: pName,
108
+ ktType: pType,
109
+ tsType: isCallback ? '() => void' : tsType(pType),
110
+ isCallback,
111
+ });
112
+ }
113
+ }
114
+ }
115
+ i = j;
116
+ break;
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ return { functions, isComponent, componentProps };
123
+ }
124
+
125
+ module.exports = { extractKotlinExports };