@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,474 @@
1
+ /**
2
+ * Compiles a .cpp/.mm file to a signed arm64 dylib.
3
+ * Called directly by the transformer (same process — no IPC needed).
4
+ */
5
+
6
+ const { execSync } = require('child_process');
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+
10
+ let _sdkPaths = {}; // 'device' → path, 'simulator' → path
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
+ console.error(`[nativ] No iOS SDK found for ${sdk}`);
23
+ }
24
+ return _sdkPaths[target] || null;
25
+ }
26
+
27
+ function resolveOnce(projectRoot) {
28
+ if (_resolved) return;
29
+ _resolved = true;
30
+
31
+ // Find the signing identity that matches the app's team ID.
32
+ try {
33
+ // Read team ID — app.json first (works without prebuild / with EAS),
34
+ // then fall back to project.pbxproj
35
+ let appTeamId = null;
36
+
37
+ const root = projectRoot || '.';
38
+ try {
39
+ const appJson = JSON.parse(fs.readFileSync(path.join(root, 'app.json'), 'utf8'));
40
+ appTeamId = appJson?.expo?.ios?.appleTeamId || null;
41
+ } catch {}
42
+
43
+ if (!appTeamId) {
44
+ try {
45
+ const pbxprojs = execSync(
46
+ `find "${root}/ios" -name "project.pbxproj" -maxdepth 3 2>/dev/null`,
47
+ { encoding: 'utf8' }
48
+ ).trim().split('\n').filter(Boolean);
49
+ for (const pbx of pbxprojs) {
50
+ try {
51
+ const content = fs.readFileSync(pbx, 'utf8');
52
+ const m = content.match(/DEVELOPMENT_TEAM\s*=\s*(\w+)/);
53
+ if (m) { appTeamId = m[1]; break; }
54
+ } catch {}
55
+ }
56
+ } catch {}
57
+ }
58
+
59
+ if (appTeamId) {
60
+ // Find the codesigning identity whose certificate OU matches the team
61
+ const identities = execSync('security find-identity -v -p codesigning', {
62
+ encoding: 'utf8',
63
+ });
64
+ const idEntries = [...identities.matchAll(/([A-F0-9]{40})\s+"([^"]+)"/g)];
65
+
66
+ for (const [, , name] of idEntries) {
67
+ try {
68
+ const certSubject = execSync(
69
+ `security find-certificate -c "${name}" -p 2>/dev/null | openssl x509 -noout -subject 2>/dev/null`,
70
+ { encoding: 'utf8' }
71
+ );
72
+ if (certSubject.includes(`OU=${appTeamId}`)) {
73
+ _signingIdentity = name;
74
+ console.log(`[nativ] Signing: ${name} (team ${appTeamId})`);
75
+ break;
76
+ }
77
+ } catch {}
78
+ }
79
+ }
80
+
81
+ // Fallback: first available identity
82
+ if (!_signingIdentity) {
83
+ const match = execSync('security find-identity -v -p codesigning', {
84
+ encoding: 'utf8',
85
+ }).match(/"(Apple Development:[^"]+)"/);
86
+ if (match) _signingIdentity = match[1];
87
+ }
88
+ } catch {}
89
+ }
90
+
91
+ /**
92
+ * Compile a .cpp/.mm file + generated bridge to a signed dylib.
93
+ * Returns the dylib path, or null on failure.
94
+ */
95
+ function compileDylib(filepath, includePaths, exports, projectRoot, { target = 'device' } = {}) {
96
+ resolveOnce(projectRoot);
97
+ const sdkPath = getSdkPath(target);
98
+ if (!sdkPath) return null;
99
+
100
+ const rel = path.relative(projectRoot, filepath);
101
+ const moduleId = rel
102
+ .replace(/\.(cpp|cc|mm)$/, '')
103
+ .replace(/[\/\\]/g, '_')
104
+ .replace(/[^a-zA-Z0-9_]/g, '_');
105
+
106
+ const outputDir = path.join(projectRoot, '.nativ/dylibs', target);
107
+ fs.mkdirSync(outputDir, { recursive: true });
108
+ const dylibPath = path.join(outputDir, `${moduleId}.dylib`);
109
+
110
+ // Generate bridge source
111
+ const bridgePath = path.join(outputDir, `${moduleId}_bridge.cpp`);
112
+ const bridgeSource = generateBridge(exports, moduleId);
113
+ fs.writeFileSync(bridgePath, bridgeSource);
114
+
115
+ const isObjCpp = filepath.endsWith('.mm');
116
+ const lang = isObjCpp ? 'objective-c++' : 'c++';
117
+
118
+ // Filter out -isysroot from include paths (we provide our own)
119
+ const filteredPaths = [];
120
+ for (let i = 0; i < includePaths.length; i++) {
121
+ if (includePaths[i] === '-isysroot') { i++; continue; }
122
+ filteredPaths.push(includePaths[i]);
123
+ }
124
+
125
+ const targetTriple = target === 'simulator'
126
+ ? 'arm64-apple-ios15.1-simulator'
127
+ : 'arm64-apple-ios15.1';
128
+
129
+ const cmd = [
130
+ 'clang++',
131
+ '-x', lang,
132
+ '-std=c++17',
133
+ '-target', targetTriple,
134
+ '-dynamiclib',
135
+ '-fPIC',
136
+ '-isysroot', sdkPath,
137
+ ...filteredPaths,
138
+ '-undefined', 'dynamic_lookup',
139
+ '-o', dylibPath,
140
+ filepath,
141
+ bridgePath,
142
+ ];
143
+
144
+ // Auto-detect frameworks from #import/#include directives
145
+ if (isObjCpp) {
146
+ const src = fs.readFileSync(filepath, 'utf8');
147
+ const frameworkImports = src.matchAll(/#(?:import|include)\s*<(\w+)\//g);
148
+ // System header dirs that are NOT frameworks
149
+ const notFrameworks = new Set([
150
+ 'sys', 'mach', 'os', 'dispatch', 'objc', 'libkern',
151
+ 'arm', 'i386', 'machine', 'net', 'netinet', 'arpa',
152
+ ]);
153
+ const frameworks = new Set();
154
+ for (const [, fw] of frameworkImports) {
155
+ if (!notFrameworks.has(fw)) {
156
+ frameworks.add(fw);
157
+ }
158
+ }
159
+ // Always include Foundation for ObjC++
160
+ frameworks.add('Foundation');
161
+ for (const fw of frameworks) {
162
+ cmd.push('-framework', fw);
163
+ }
164
+ }
165
+
166
+ // Suppress deprecation warnings — they cause noise but aren't errors
167
+ cmd.push('-Wno-deprecated-declarations');
168
+
169
+ try {
170
+ execSync(cmd.join(' '), { stdio: 'pipe', encoding: 'utf8' });
171
+ } catch (err) {
172
+ console.error(`[nativ] dylib compile failed: ${path.basename(filepath)}`);
173
+ console.error((err.stderr || '').slice(0, 1000));
174
+ return null;
175
+ }
176
+
177
+ // Sign with the same identity/team as the app
178
+ if (_signingIdentity) {
179
+ try {
180
+ execSync(`codesign -fs "${_signingIdentity}" "${dylibPath}"`, { stdio: 'pipe' });
181
+ } catch {
182
+ // Fallback: ad-hoc sign
183
+ try {
184
+ execSync(`codesign -fs - "${dylibPath}"`, { stdio: 'pipe' });
185
+ } catch {}
186
+ }
187
+ }
188
+
189
+ const size = fs.statSync(dylibPath).size;
190
+ console.log(`[nativ] Built ${moduleId}.dylib (${(size / 1024).toFixed(1)}KB)`);
191
+ return dylibPath;
192
+ }
193
+
194
+ function generateBridge(exports, moduleId) {
195
+ const needsDispatch = exports.some(fn => fn.mainThread);
196
+ const lines = [
197
+ '// Hot-reload bridge — auto-generated',
198
+ '#include <string>',
199
+ '#include <cstdlib>',
200
+ ...(needsDispatch ? ['#include <dispatch/dispatch.h>'] : []),
201
+ '',
202
+ '// Forward declarations',
203
+ ];
204
+
205
+ for (const fn of exports) {
206
+ const argTypes = fn.args.map(a => a.type + ' ' + a.name).join(', ');
207
+ lines.push(`extern ${fn.ret} ${fn.name}(${argTypes});`);
208
+ }
209
+
210
+ lines.push('', 'extern "C" {');
211
+ lines.push('typedef const char* (*NativSyncFn)(const char*);');
212
+ lines.push('typedef void (*NativAsyncFn)(const char*, void (*)(const char*), void (*)(const char*, const char*));');
213
+ lines.push('void nativ_register_sync(const char*, const char*, NativSyncFn);');
214
+ lines.push('void nativ_register_async(const char*, const char*, NativAsyncFn);');
215
+ lines.push('');
216
+
217
+ // JSON string escaping helper
218
+ lines.push(String.raw`static std::string _jsonEscapeString(const std::string& s) {
219
+ std::string buf = "\"";
220
+ for (unsigned char c : s) {
221
+ if (c == '"') buf += "\\\"";
222
+ else if (c == '\\') buf += "\\\\";
223
+ else if (c == '\n') buf += "\\n";
224
+ else if (c == '\r') buf += "\\r";
225
+ else if (c == '\t') buf += "\\t";
226
+ else if (c >= 0x20) buf += (char)c;
227
+ }
228
+ buf += "\"";
229
+ return buf;
230
+ }`);
231
+ lines.push('');
232
+
233
+ // JSON parse helpers
234
+ lines.push('static double _parseNumber(const char* &p) {');
235
+ lines.push(' while (*p == \' \' || *p == \',\' || *p == \'[\') p++;');
236
+ lines.push(' char* end; double v = strtod(p, &end); p = end; return v;');
237
+ lines.push('}');
238
+ lines.push('static std::string _parseString(const char* &p) {');
239
+ lines.push(' while (*p && *p != \'"\') p++; if (*p == \'"\') p++;');
240
+ lines.push(' std::string s; while (*p && *p != \'"\') {');
241
+ lines.push(' if (*p == \'\\\\\' && *(p+1)) { p++; s += *p; } else { s += *p; } p++; }');
242
+ lines.push(' if (*p == \'"\') p++; return s;');
243
+ lines.push('}');
244
+ lines.push('');
245
+
246
+ for (const fn of exports) {
247
+ if (fn.async) {
248
+ // Async wrapper: receives argsJson + resolve/reject callbacks
249
+ lines.push(`static void nativ_cpp_async_${moduleId}_${fn.name}(const char* argsJson, void (*resolve)(const char*), void (*reject)(const char*, const char*)) {`);
250
+ lines.push(' const char* p = argsJson;');
251
+ lines.push(' while (*p && *p != \'[\') p++; if (*p == \'[\') p++;');
252
+
253
+ const argNames = [];
254
+ for (const arg of fn.args) {
255
+ const t = arg.type.replace(/const\s+/, '').replace(/\s*&\s*$/, '').trim();
256
+ if (t === 'std::string') {
257
+ lines.push(` std::string ${arg.name} = _parseString(p);`);
258
+ } else {
259
+ lines.push(` ${arg.type} ${arg.name} = (${arg.type})_parseNumber(p);`);
260
+ }
261
+ argNames.push(arg.name);
262
+ }
263
+
264
+ lines.push(' try {');
265
+ lines.push(` auto result = ${fn.name}(${argNames.join(', ')});`);
266
+
267
+ const retBase = fn.ret.replace(/const\s+/, '').replace(/\s*&\s*$/, '').trim();
268
+ if (retBase === 'std::string') {
269
+ lines.push(' resolve(_jsonEscapeString(result).c_str());');
270
+ } else if (retBase === 'void') {
271
+ lines.push(' resolve("null");');
272
+ } else {
273
+ lines.push(' std::string buf = std::to_string(result);');
274
+ lines.push(' resolve(buf.c_str());');
275
+ }
276
+ lines.push(' } catch (const std::exception& e) {');
277
+ lines.push(' reject("NATIVE_ERROR", e.what());');
278
+ lines.push(' } catch (...) {');
279
+ lines.push(' reject("NATIVE_ERROR", "Unknown error");');
280
+ lines.push(' }');
281
+ lines.push('}');
282
+ lines.push('');
283
+ continue;
284
+ }
285
+
286
+ lines.push(`static const char* nativ_cpp_${moduleId}_${fn.name}(const char* argsJson) {`);
287
+ lines.push(' const char* p = argsJson;');
288
+ lines.push(' while (*p && *p != \'[\') p++; if (*p == \'[\') p++;');
289
+
290
+ const argNames = [];
291
+ for (const arg of fn.args) {
292
+ const t = arg.type.replace(/const\s+/, '').replace(/\s*&\s*$/, '').trim();
293
+ if (t === 'std::string') {
294
+ lines.push(` std::string ${arg.name} = _parseString(p);`);
295
+ } else {
296
+ lines.push(` ${arg.type} ${arg.name} = (${arg.type})_parseNumber(p);`);
297
+ }
298
+ argNames.push(arg.name);
299
+ }
300
+
301
+ const retBase = fn.ret.replace(/const\s+/, '').replace(/\s*&\s*$/, '').trim();
302
+
303
+ // main thread dispatch for UIKit-touching functions
304
+ if (fn.mainThread) {
305
+ lines.push(' static thread_local std::string buf;');
306
+ lines.push(' __block std::string _result;');
307
+ lines.push(' dispatch_sync(dispatch_get_main_queue(), ^{');
308
+ lines.push(` auto result = ${fn.name}(${argNames.join(', ')});`);
309
+ if (retBase === 'std::string') {
310
+ lines.push(' _result = _jsonEscapeString(result);');
311
+ } else {
312
+ lines.push(' _result = std::to_string(result);');
313
+ }
314
+ lines.push(' });');
315
+ lines.push(' buf = _result;');
316
+ lines.push(' return buf.c_str();');
317
+ } else {
318
+ lines.push(` auto result = ${fn.name}(${argNames.join(', ')});`);
319
+ if (retBase === 'std::string') {
320
+ lines.push(' static thread_local std::string buf;');
321
+ lines.push(' buf = _jsonEscapeString(result);');
322
+ lines.push(' return buf.c_str();');
323
+ } else {
324
+ lines.push(' static thread_local std::string buf;');
325
+ lines.push(' buf = std::to_string(result);');
326
+ lines.push(' return buf.c_str();');
327
+ }
328
+ }
329
+ lines.push('}');
330
+ lines.push('');
331
+ }
332
+
333
+ // Constructor
334
+ lines.push('__attribute__((constructor))');
335
+ lines.push(`static void nativ_cpp_register_${moduleId}() {`);
336
+ for (const fn of exports) {
337
+ if (fn.async) {
338
+ lines.push(` nativ_register_async("${moduleId}", "${fn.name}", nativ_cpp_async_${moduleId}_${fn.name});`);
339
+ } else {
340
+ lines.push(` nativ_register_sync("${moduleId}", "${fn.name}", nativ_cpp_${moduleId}_${fn.name});`);
341
+ }
342
+ }
343
+ lines.push('}');
344
+ lines.push('');
345
+ lines.push('} // extern "C"');
346
+
347
+ return lines.join('\n');
348
+ }
349
+
350
+ /**
351
+ * Compile an ObjC++/C++ component file to a signed dylib.
352
+ * The file defines a props struct + mount() function.
353
+ * The bridge auto-generates nativ_render that extracts props from the snapshot.
354
+ */
355
+ function compileCppComponentDylib(filepath, includePaths, projectRoot, baseName, componentProps, { target = 'device' } = {}) {
356
+ resolveOnce(projectRoot);
357
+ const sdkPath = getSdkPath(target);
358
+ if (!sdkPath) return null;
359
+
360
+ const componentId = `nativ.${baseName}`;
361
+ const outputDir = path.join(projectRoot, '.nativ/dylibs', target);
362
+ fs.mkdirSync(outputDir, { recursive: true });
363
+ const dylibPath = path.join(outputDir, `nativ_${baseName}.dylib`);
364
+
365
+ // Find the props struct name from NATIV_COMPONENT macro
366
+ const src = fs.readFileSync(filepath, 'utf8');
367
+ const compMatch = src.match(/NATIV_COMPONENT\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)/);
368
+ const propsTypeName = compMatch ? compMatch[2] : null;
369
+
370
+ // Generate prop extraction code
371
+ const propExtractions = (componentProps || []).map(p => {
372
+ if (p.cppType === 'std::string') {
373
+ return ` props.${p.name} = _nativ_get_string(rt, obj, "${p.jsName}", props.${p.name});`;
374
+ } else if (p.cppType === 'double' || p.cppType === 'float' || p.cppType === 'int') {
375
+ return ` props.${p.name} = _nativ_get_number(rt, obj, "${p.jsName}", props.${p.name});`;
376
+ } else if (p.cppType === 'bool') {
377
+ return ` props.${p.name} = _nativ_get_bool(rt, obj, "${p.jsName}", props.${p.name});`;
378
+ } else if (p.cppType === 'std::function<void()>') {
379
+ return ` props.${p.name} = _nativ_get_callback(rt, obj, "${p.jsName}");`;
380
+ }
381
+ return ` // unknown type for ${p.name}: ${p.cppType}`;
382
+ }).join('\n');
383
+
384
+ // Generate bridge that auto-extracts props and calls mount()
385
+ const bridgePath = path.join(outputDir, `nativ_${baseName}_bridge.mm`);
386
+ const relPath = path.relative(outputDir, filepath);
387
+ const renderFnName = `nativ_${baseName}_render`;
388
+ const bridgeSrc = `
389
+ // Auto-generated component bridge for ${baseName}
390
+ #include "${relPath}"
391
+
392
+ // Auto-generated render function — unique per component for static linking compat
393
+ extern "C"
394
+ void ${renderFnName}(void* view, float width, float height,
395
+ void* jsi_runtime, void* jsi_props) {
396
+ void* rt = jsi_runtime;
397
+ void* obj = jsi_props;
398
+ ${propsTypeName ? ` ${propsTypeName} props;
399
+ ${propExtractions}
400
+ mount(view, width, height, props);` : ` mount(view, width, height);`}
401
+ }
402
+
403
+ extern "C" {
404
+ typedef void (*NativRenderFn)(void*, float, float, void*, void*);
405
+ void nativ_register_render(const char*, NativRenderFn);
406
+ }
407
+
408
+ __attribute__((constructor))
409
+ static void register_${baseName}() {
410
+ nativ_register_render("${componentId}", ${renderFnName});
411
+ }
412
+ `;
413
+ fs.writeFileSync(bridgePath, bridgeSrc);
414
+
415
+ const isObjCpp = filepath.endsWith('.mm');
416
+ const lang = isObjCpp ? 'objective-c++' : 'c++';
417
+
418
+ const filteredPaths = [];
419
+ for (let i = 0; i < includePaths.length; i++) {
420
+ if (includePaths[i] === '-isysroot') { i++; continue; }
421
+ filteredPaths.push(includePaths[i]);
422
+ }
423
+
424
+ const targetTriple = target === 'simulator'
425
+ ? 'arm64-apple-ios15.1-simulator'
426
+ : 'arm64-apple-ios15.1';
427
+
428
+ const cmd = [
429
+ 'clang++',
430
+ '-x', lang,
431
+ '-std=c++17',
432
+ '-target', targetTriple,
433
+ '-dynamiclib',
434
+ '-fPIC',
435
+ '-isysroot', sdkPath,
436
+ ...filteredPaths,
437
+ '-undefined', 'dynamic_lookup',
438
+ '-Wno-deprecated-declarations',
439
+ '-o', dylibPath,
440
+ filepath,
441
+ bridgePath,
442
+ ];
443
+
444
+ // Auto-detect frameworks
445
+ if (isObjCpp) {
446
+ const src = fs.readFileSync(filepath, 'utf8');
447
+ const notFrameworks = new Set(['sys', 'mach', 'os', 'dispatch', 'objc', 'libkern', 'arm', 'i386', 'machine', 'net', 'netinet', 'arpa']);
448
+ const frameworks = new Set(['Foundation']);
449
+ for (const [, fw] of src.matchAll(/#(?:import|include)\s*<(\w+)\//g)) {
450
+ if (!notFrameworks.has(fw)) frameworks.add(fw);
451
+ }
452
+ for (const fw of frameworks) cmd.push('-framework', fw);
453
+ }
454
+
455
+ try {
456
+ execSync(cmd.join(' '), { stdio: 'pipe', encoding: 'utf8' });
457
+ } catch (err) {
458
+ console.error(`[nativ] Component dylib compile failed: ${path.basename(filepath)}`);
459
+ console.error((err.stderr || '').slice(0, 1000));
460
+ return null;
461
+ }
462
+
463
+ if (_signingIdentity) {
464
+ try {
465
+ execSync(`codesign -fs "${_signingIdentity}" "${dylibPath}"`, { stdio: 'pipe' });
466
+ } catch {}
467
+ }
468
+
469
+ const size = fs.statSync(dylibPath).size;
470
+ console.log(`[nativ] Built nativ_${baseName}.dylib component (${(size / 1024).toFixed(1)}KB)`);
471
+ return dylibPath;
472
+ }
473
+
474
+ module.exports = { compileDylib, compileCppComponentDylib, generateBridge };