@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.
- package/NativFabric.podspec +41 -0
- package/android/build.gradle +128 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/CMakeLists.txt +59 -0
- package/android/src/main/cpp/NativBindingsInstaller.cpp +393 -0
- package/android/src/main/cpp/NativRuntime.cpp +508 -0
- package/android/src/main/java/com/nativfabric/ComposeHost.kt +26 -0
- package/android/src/main/java/com/nativfabric/NativContainerPackage.kt +35 -0
- package/android/src/main/java/com/nativfabric/NativContainerView.kt +51 -0
- package/android/src/main/java/com/nativfabric/NativContainerViewManager.kt +62 -0
- package/android/src/main/java/com/nativfabric/NativRuntime.kt +201 -0
- package/android/src/main/java/com/nativfabric/NativRuntimeModule.kt +37 -0
- package/android/src/main/java/com/nativfabric/compose/ComposeWrappers.kt +45 -0
- package/app.plugin.js +159 -0
- package/expo-module.config.json +6 -0
- package/ios/NativContainerComponentView.mm +137 -0
- package/ios/NativRuntime.h +36 -0
- package/ios/NativRuntime.mm +549 -0
- package/metro/Nativ.h +126 -0
- package/metro/compilers/android-compiler.js +339 -0
- package/metro/compilers/dylib-compiler.js +474 -0
- package/metro/compilers/kotlin-compiler.js +632 -0
- package/metro/compilers/rust-compiler.js +722 -0
- package/metro/compilers/static-compiler.js +1118 -0
- package/metro/compilers/swift-compiler.js +363 -0
- package/metro/extractors/cpp-ast-extractor.js +126 -0
- package/metro/extractors/kotlin-extractor.js +125 -0
- package/metro/extractors/rust-extractor.js +118 -0
- package/metro/index.js +236 -0
- package/metro/transformer.js +649 -0
- package/metro/utils/bridge-generator.js +50 -0
- package/metro/utils/compile-commands.js +104 -0
- package/metro/utils/cpp-daemon.js +344 -0
- package/metro/utils/dts-generator.js +32 -0
- package/metro/utils/include-resolver.js +73 -0
- package/metro/utils/kotlin-daemon.js +394 -0
- package/metro/utils/type-mapper.js +63 -0
- package/package.json +43 -0
- package/react-native.config.js +13 -0
- package/src/NativContainerNativeComponent.ts +9 -0
- package/src/NativeNativRuntime.ts +8 -0
- 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 };
|