@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,1118 @@
1
+ /**
2
+ * static-compiler.js — Production build: generates bridges + compiles Rust.
3
+ *
4
+ * iOS (CocoaPods :before_compile):
5
+ * Three fixed-name files in --output dir:
6
+ * - nativ_bridges.mm — all C++/ObjC++ bridges + Swift/Rust C registration
7
+ * - nativ_bridges.swift — all Swift source + @_cdecl wrappers
8
+ * - libnativ_user.a — unified Rust static library
9
+ *
10
+ * Android (Gradle pre-build):
11
+ * Per-file bridges + Kotlin wrappers in .nativ/generated/
12
+ *
13
+ * Invoked by:
14
+ * - CocoaPods script phase: node static-compiler.js --platform ios --root $ROOT --output $DIR
15
+ * - Gradle pre-build task: node static-compiler.js --platform android --root $ROOT
16
+ */
17
+
18
+ const path = require("path");
19
+ const fs = require("fs");
20
+ const { execSync } = require("child_process");
21
+
22
+ // Reuse existing extractors
23
+ const {
24
+ extractCppExports,
25
+ isCppComponent,
26
+ extractCppComponentProps,
27
+ } = require("../extractors/cpp-ast-extractor");
28
+ // extractRustExports used via lazy require in buildRustStatic
29
+ const { extractSwiftExports } = require("./swift-compiler");
30
+ const { extractKotlinExports } = require("../extractors/kotlin-extractor");
31
+
32
+ // Reuse existing bridge generators (Android per-file path)
33
+ const { generateBridge } = require("./dylib-compiler");
34
+
35
+ // ─── CLI ───────────────────────────────────────────────────────────────
36
+
37
+ // Skip in Debug builds — all native code loads via Metro dylibs
38
+ if (process.env.CONFIGURATION === "Debug") {
39
+ console.log("[nativ] Debug build — skipping static compilation");
40
+ process.exit(0);
41
+ }
42
+
43
+ const args = process.argv.slice(2);
44
+ const platformIdx = args.indexOf("--platform");
45
+ const platform = platformIdx >= 0 ? args[platformIdx + 1] : "ios";
46
+ const projectRoot = args.includes("--root")
47
+ ? args[args.indexOf("--root") + 1]
48
+ : process.cwd();
49
+ const outputDir = args.includes("--output")
50
+ ? args[args.indexOf("--output") + 1]
51
+ : null;
52
+
53
+ const isIOS = platform === "ios";
54
+ const isAndroid = platform === "android";
55
+
56
+ // iOS: output is the bridges directory (three fixed files)
57
+ // Android: output is .nativ/generated/ with subdirectories
58
+ const genDir = path.join(projectRoot, ".nativ/generated");
59
+ let bridgeDir, releaseDir;
60
+ if (isIOS) {
61
+ bridgeDir = outputDir || path.join(projectRoot, ".nativ/bridges");
62
+ releaseDir = bridgeDir; // libnativ_user.a goes alongside bridges
63
+ } else {
64
+ bridgeDir = path.join(genDir, "bridges/android");
65
+ releaseDir = path.join(genDir, "release");
66
+ }
67
+ fs.mkdirSync(bridgeDir, { recursive: true });
68
+ if (isAndroid) fs.mkdirSync(releaseDir, { recursive: true });
69
+
70
+ console.log(
71
+ `[nativ] Static compiler: platform=${platform}, root=${projectRoot}`,
72
+ );
73
+
74
+ // ─── Scan for user native files ────────────────────────────────────────
75
+
76
+ function findUserFiles(exts) {
77
+ const results = [];
78
+ const ignore = [
79
+ "node_modules",
80
+ ".nativ",
81
+ "modules",
82
+ "ios",
83
+ "android",
84
+ "vendor",
85
+ ];
86
+
87
+ function walk(dir) {
88
+ let entries;
89
+ try {
90
+ entries = fs.readdirSync(dir, { withFileTypes: true });
91
+ } catch {
92
+ return;
93
+ }
94
+ for (const entry of entries) {
95
+ if (ignore.includes(entry.name)) continue;
96
+ const full = path.join(dir, entry.name);
97
+ if (entry.isDirectory()) {
98
+ walk(full);
99
+ } else if (exts.some((ext) => entry.name.endsWith(ext))) {
100
+ results.push(full);
101
+ }
102
+ }
103
+ }
104
+ walk(projectRoot);
105
+ return results;
106
+ }
107
+
108
+ // ─── iOS: combined bridge generation ──────────────────────────────────
109
+ // All bridges go into two files: nativ_bridges.mm + nativ_bridges.swift
110
+
111
+ function buildCombinedIOSBridges() {
112
+ const mmSections = [];
113
+
114
+ // Shared .mm preamble
115
+ mmSections.push(`// Auto-generated by React Native Native — do not edit
116
+ // Combined production bridges for all native modules
117
+
118
+ #include <string>
119
+ #include <cstdlib>
120
+ #include <dispatch/dispatch.h>
121
+
122
+ // Forward declarations
123
+ extern "C" {
124
+ typedef const char* (*NativSyncFn)(const char*);
125
+ typedef void (*NativAsyncFn)(const char*, void (*)(const char*), void (*)(const char*, const char*));
126
+ typedef void (*NativRenderFn)(void*, float, float, void*, void*);
127
+ void nativ_register_sync(const char*, const char*, NativSyncFn);
128
+ void nativ_register_async(const char*, const char*, NativAsyncFn);
129
+ void nativ_register_render(const char*, NativRenderFn);
130
+ }
131
+
132
+ // JSON helpers
133
+ ${generateJsonHelpers()}
134
+ `);
135
+
136
+ // ── C++/ObjC++ modules ─────────────────────────────────────
137
+ const cppFiles = findUserFiles([".cpp", ".cc", ".mm"]);
138
+ for (const filepath of cppFiles) {
139
+ if (isCppComponent(filepath)) {
140
+ mmSections.push(generateCppComponentSection(filepath));
141
+ } else {
142
+ const exports = extractCppExports(filepath, []);
143
+ if (exports.length === 0) continue;
144
+ const rel = path.relative(projectRoot, filepath);
145
+ const moduleId = rel
146
+ .replace(/\.(cpp|cc|mm)$/, "")
147
+ .replace(/[\/\\]/g, "_")
148
+ .replace(/[^a-zA-Z0-9_]/g, "_");
149
+ mmSections.push(generateCppModuleSection(filepath, exports, moduleId));
150
+ }
151
+ }
152
+
153
+ // ── Swift modules ──────────────────────────────────────────
154
+ const swiftFiles = findUserFiles([".swift"]);
155
+ const swiftBodies = [];
156
+ const swiftWrappers = [];
157
+
158
+ for (const filepath of swiftFiles) {
159
+ const name = path.basename(filepath, ".swift");
160
+ const moduleId = name.toLowerCase();
161
+ const src = fs.readFileSync(filepath, "utf8");
162
+ const isComp =
163
+ src.includes("@nativ_component") || src.includes("nativ::component");
164
+
165
+ if (isComp) {
166
+ // Component: generate render bridge in Swift, C registration in .mm
167
+ mmSections.push(generateSwiftComponentRegistration(moduleId));
168
+ const { swiftCode } = generateSwiftComponentBridge(
169
+ filepath,
170
+ src,
171
+ name,
172
+ moduleId,
173
+ );
174
+ swiftBodies.push(swiftCode);
175
+ console.log(`[nativ] Bridge: ${moduleId} (Swift component)`);
176
+ } else {
177
+ const exports = extractSwiftExports(filepath);
178
+
179
+ if (exports.length === 0) {
180
+ // No annotations — include as helper file (available to annotated files)
181
+ swiftBodies.push(`// From: ${name}.swift\n${stripAnnotations(src)}`);
182
+ console.log(`[nativ] Swift helper: ${name}.swift`);
183
+ continue;
184
+ }
185
+
186
+ // C registration in .mm
187
+ mmSections.push(generateSwiftFunctionRegistration(exports, moduleId));
188
+
189
+ // User source (imports kept) + @_cdecl wrappers
190
+ swiftBodies.push(`// From: ${name}.swift\n${stripAnnotations(src)}`);
191
+ swiftWrappers.push(
192
+ ...exports.map((fn) => generateSwiftCdeclWrapper(fn)),
193
+ );
194
+ console.log(
195
+ `[nativ] Bridge: ${moduleId} (${exports.length} Swift functions)`,
196
+ );
197
+ }
198
+ }
199
+
200
+ // ── Write nativ_bridges.mm ─────────────────────────────────
201
+ const mmContent = mmSections.join("\n");
202
+ fs.writeFileSync(path.join(bridgeDir, "nativ_bridges.mm"), mmContent);
203
+ console.log(
204
+ `[nativ] Wrote nativ_bridges.mm (${(mmContent.length / 1024).toFixed(1)}KB)`,
205
+ );
206
+
207
+ // ── Write nativ_bridges.swift ──────────────────────────────
208
+ const swiftContent = [
209
+ "// Auto-generated by React Native Native — do not edit",
210
+ "",
211
+ ...swiftBodies,
212
+ "",
213
+ ...swiftWrappers,
214
+ "",
215
+ ].join("\n");
216
+ fs.writeFileSync(path.join(bridgeDir, "nativ_bridges.swift"), swiftContent);
217
+ console.log(
218
+ `[nativ] Wrote nativ_bridges.swift (${(swiftContent.length / 1024).toFixed(1)}KB)`,
219
+ );
220
+ }
221
+
222
+ // ─── Helpers: JSON parse/escape (emitted once in combined .mm) ────────
223
+
224
+ function generateJsonHelpers() {
225
+ return String.raw`static std::string _jsonEscapeString(const std::string& s) {
226
+ std::string buf = "\"";
227
+ for (unsigned char c : s) {
228
+ if (c == '"') buf += "\\\"";
229
+ else if (c == '\\') buf += "\\\\";
230
+ else if (c == '\n') buf += "\\n";
231
+ else if (c == '\r') buf += "\\r";
232
+ else if (c == '\t') buf += "\\t";
233
+ else if (c >= 0x20) buf += (char)c;
234
+ }
235
+ buf += "\"";
236
+ return buf;
237
+ }
238
+
239
+ static double _parseNumber(const char* &p) {
240
+ while (*p == ' ' || *p == ',' || *p == '[') p++;
241
+ char* end; double v = strtod(p, &end); p = end; return v;
242
+ }
243
+
244
+ static std::string _parseString(const char* &p) {
245
+ while (*p && *p != '"') p++; if (*p == '"') p++;
246
+ std::string s; while (*p && *p != '"') {
247
+ if (*p == '\\' && *(p+1)) { p++; s += *p; } else { s += *p; } p++; }
248
+ if (*p == '"') p++; return s;
249
+ }`;
250
+ }
251
+
252
+ // ─── Helpers: per-module C++ bridge code ──────────────────────────────
253
+
254
+ function generateCppModuleSection(filepath, exports, moduleId) {
255
+ const lines = [];
256
+
257
+ lines.push(`// ─── C++ module: ${moduleId} ────────────────────────────`);
258
+ lines.push(`#include "${path.resolve(filepath)}"`);
259
+ lines.push("");
260
+ lines.push("extern \"C\" {");
261
+
262
+ for (const fn of exports) {
263
+ lines.push(...generateCppFnWrapper(fn, moduleId));
264
+ lines.push("");
265
+ }
266
+
267
+ // Constructor
268
+ lines.push("__attribute__((constructor, used))");
269
+ lines.push(`static void nativ_cpp_register_${moduleId}() {`);
270
+ for (const fn of exports) {
271
+ if (fn.async) {
272
+ lines.push(
273
+ ` nativ_register_async("${moduleId}", "${fn.name}", nativ_cpp_async_${moduleId}_${fn.name});`,
274
+ );
275
+ } else {
276
+ lines.push(
277
+ ` nativ_register_sync("${moduleId}", "${fn.name}", nativ_cpp_${moduleId}_${fn.name});`,
278
+ );
279
+ }
280
+ }
281
+ lines.push("}");
282
+ lines.push("} // extern \"C\"");
283
+ lines.push("");
284
+
285
+ console.log(`[nativ] Bridge: ${moduleId} (${exports.length} C++ functions)`);
286
+ return lines.join("\n");
287
+ }
288
+
289
+ function generateCppFnWrapper(fn, moduleId) {
290
+ const lines = [];
291
+ const retBase = fn.ret
292
+ .replace(/const\s+/, "")
293
+ .replace(/\s*&\s*$/, "")
294
+ .trim();
295
+
296
+ if (fn.async) {
297
+ lines.push(
298
+ `static void nativ_cpp_async_${moduleId}_${fn.name}(const char* argsJson, void (*resolve)(const char*), void (*reject)(const char*, const char*)) {`,
299
+ );
300
+ lines.push(...generateArgParsing(fn.args));
301
+ lines.push(" try {");
302
+ const call = `${fn.name}(${fn.args.map((a) => a.name).join(", ")})`;
303
+ if (retBase === "void") {
304
+ lines.push(` ${call};`);
305
+ lines.push(' resolve("null");');
306
+ } else if (retBase === "std::string") {
307
+ lines.push(` auto result = ${call};`);
308
+ lines.push(" resolve(_jsonEscapeString(result).c_str());");
309
+ } else {
310
+ lines.push(` auto result = ${call};`);
311
+ lines.push(" std::string buf = std::to_string(result);");
312
+ lines.push(" resolve(buf.c_str());");
313
+ }
314
+ lines.push(" } catch (const std::exception& e) {");
315
+ lines.push(' reject("NATIVE_ERROR", e.what());');
316
+ lines.push(" } catch (...) {");
317
+ lines.push(' reject("NATIVE_ERROR", "Unknown error");');
318
+ lines.push(" }");
319
+ lines.push("}");
320
+ return lines;
321
+ }
322
+
323
+ lines.push(
324
+ `static const char* nativ_cpp_${moduleId}_${fn.name}(const char* argsJson) {`,
325
+ );
326
+ lines.push(...generateArgParsing(fn.args));
327
+
328
+ const call = `${fn.name}(${fn.args.map((a) => a.name).join(", ")})`;
329
+
330
+ if (fn.mainThread) {
331
+ lines.push(" static thread_local std::string buf;");
332
+ lines.push(" __block std::string _result;");
333
+ lines.push(" dispatch_sync(dispatch_get_main_queue(), ^{");
334
+ lines.push(` auto result = ${call};`);
335
+ if (retBase === "std::string") {
336
+ lines.push(" _result = _jsonEscapeString(result);");
337
+ } else {
338
+ lines.push(" _result = std::to_string(result);");
339
+ }
340
+ lines.push(" });");
341
+ lines.push(" buf = _result;");
342
+ lines.push(" return buf.c_str();");
343
+ } else {
344
+ lines.push(` auto result = ${call};`);
345
+ lines.push(" static thread_local std::string buf;");
346
+ if (retBase === "std::string") {
347
+ lines.push(" buf = _jsonEscapeString(result);");
348
+ } else {
349
+ lines.push(" buf = std::to_string(result);");
350
+ }
351
+ lines.push(" return buf.c_str();");
352
+ }
353
+
354
+ lines.push("}");
355
+ return lines;
356
+ }
357
+
358
+ function generateArgParsing(fnArgs) {
359
+ const lines = [];
360
+ lines.push(" const char* p = argsJson;");
361
+ lines.push(" while (*p && *p != '[') p++; if (*p == '[') p++;");
362
+ for (const arg of fnArgs) {
363
+ const t = arg.type
364
+ .replace(/const\s+/, "")
365
+ .replace(/\s*&\s*$/, "")
366
+ .trim();
367
+ if (t === "std::string") {
368
+ lines.push(` std::string ${arg.name} = _parseString(p);`);
369
+ } else {
370
+ lines.push(` ${arg.type} ${arg.name} = (${arg.type})_parseNumber(p);`);
371
+ }
372
+ }
373
+ return lines;
374
+ }
375
+
376
+ // ─── Helpers: C++ component bridge ───────────────────────────────────
377
+
378
+ function generateCppComponentSection(filepath) {
379
+ const baseName = path
380
+ .basename(filepath)
381
+ .replace(/\.(cpp|cc|mm)$/, "")
382
+ .toLowerCase();
383
+ const cppProps = extractCppComponentProps(filepath);
384
+ const propsTypeName = (() => {
385
+ const src = fs.readFileSync(filepath, "utf8");
386
+ const m = src.match(/NATIV_COMPONENT\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)/);
387
+ return m ? m[2] : null;
388
+ })();
389
+
390
+ const propExtractions = (cppProps || [])
391
+ .map((p) => {
392
+ if (p.cppType === "std::string")
393
+ return ` props.${p.name} = _nativ_get_string(rt, obj, "${p.jsName}", props.${p.name});`;
394
+ if (["double", "float", "int"].includes(p.cppType))
395
+ return ` props.${p.name} = _nativ_get_number(rt, obj, "${p.jsName}", props.${p.name});`;
396
+ if (p.cppType === "bool")
397
+ return ` props.${p.name} = _nativ_get_bool(rt, obj, "${p.jsName}", props.${p.name});`;
398
+ return "";
399
+ })
400
+ .join("\n");
401
+
402
+ const renderFnName = `nativ_${baseName}_render`;
403
+
404
+ console.log(`[nativ] Bridge: ${baseName} (C++ component)`);
405
+
406
+ return `// ─── C++ component: ${baseName} ─────────────────────────
407
+ #include "${path.resolve(filepath)}"
408
+
409
+ extern "C"
410
+ void ${renderFnName}(void* view, float width, float height,
411
+ void* jsi_runtime, void* jsi_props) {
412
+ void* rt = jsi_runtime;
413
+ void* obj = jsi_props;
414
+ ${propsTypeName ? ` ${propsTypeName} props;\n${propExtractions}\n mount(view, width, height, props);` : " mount(view, width, height);"}
415
+ }
416
+ `;
417
+ }
418
+
419
+ // ─── Helpers: Swift registration in .mm ──────────────────────────────
420
+
421
+ function generateSwiftFunctionRegistration(exports, moduleId) {
422
+ const declarations = exports
423
+ .map(
424
+ (fn) =>
425
+ `extern const char* ${fn.cdeclName}(const char*);`,
426
+ )
427
+ .join("\n");
428
+ const registrations = exports
429
+ .map(
430
+ (fn) =>
431
+ ` nativ_register_sync("${moduleId}", "${fn.name}", ${fn.cdeclName});`,
432
+ )
433
+ .join("\n");
434
+
435
+ return `// ─── Swift module: ${moduleId} ──────────────────────────
436
+ extern "C" {
437
+ ${declarations}
438
+
439
+ __attribute__((constructor, used))
440
+ static void nativ_register_${moduleId}() {
441
+ ${registrations}
442
+ }
443
+ } // extern "C"
444
+ `;
445
+ }
446
+
447
+ function generateSwiftComponentRegistration(moduleId) {
448
+ const renderFnName = `nativ_${moduleId}_render`;
449
+ return `// ─── Swift component: ${moduleId} ──────────────────────
450
+ extern "C" {
451
+ extern void ${renderFnName}(void*, float, float, void*, void*);
452
+
453
+ __attribute__((constructor, used))
454
+ static void nativ_register_${moduleId}() {
455
+ nativ_register_render("nativ.${moduleId}", ${renderFnName});
456
+ }
457
+ } // extern "C"
458
+ `;
459
+ }
460
+
461
+ // ─── Helpers: Swift @_cdecl wrappers ─────────────────────────────────
462
+
463
+ function generateSwiftCdeclWrapper(fn) {
464
+ const retType = fn.ret || "Void";
465
+ const argPassthrough = fn.args.map((a) => a.name).join(", ");
466
+
467
+ let resultExpr;
468
+ if (retType === "String")
469
+ resultExpr = `return UnsafePointer(strdup("\\"" + result + "\\"")!)`;
470
+ else if (retType === "Bool")
471
+ resultExpr = `return UnsafePointer(strdup(result ? "true" : "false")!)`;
472
+ else if (retType === "Void")
473
+ resultExpr = `return UnsafePointer(strdup("null")!)`;
474
+ else resultExpr = `return UnsafePointer(strdup(String(result))!)`;
475
+
476
+ if (fn.mainThread) {
477
+ return `
478
+ @_cdecl("${fn.cdeclName}")
479
+ func _nativ_${fn.name}(_ argsJson: UnsafePointer<CChar>) -> UnsafePointer<CChar> {
480
+ var ptr: UnsafePointer<CChar>!
481
+ DispatchQueue.main.sync {
482
+ let result = ${fn.name}(${argPassthrough})
483
+ ptr = ${resultExpr.replace("return ", "")}
484
+ }
485
+ return ptr
486
+ }`;
487
+ }
488
+
489
+ return `
490
+ @_cdecl("${fn.cdeclName}")
491
+ func _nativ_${fn.name}(_ argsJson: UnsafePointer<CChar>) -> UnsafePointer<CChar> {
492
+ let result = ${fn.name}(${argPassthrough})
493
+ ${resultExpr}
494
+ }`;
495
+ }
496
+
497
+ // ─── Helpers: Swift component bridge ─────────────────────────────────
498
+
499
+ function generateSwiftComponentBridge(_filepath, src, name, moduleId) {
500
+ const structMatch = src.match(
501
+ /\/\/\s*@nativ_component\s*\n\s*struct\s+(\w+)\s*:\s*View\s*\{([\s\S]*?)var\s+body\s*:\s*some\s+View/,
502
+ );
503
+ const structName = structMatch ? structMatch[1] : name;
504
+ const propsBlock = structMatch ? structMatch[2] : "";
505
+ // Parse fields
506
+ const props = [];
507
+ for (const line of propsBlock.split("\n")) {
508
+ const m = line.trim().match(/(?:let|var)\s+(\w+)\s*:\s*(\w+)/);
509
+ if (m && m[1] !== "body") {
510
+ props.push({ name: m[1], type: m[2] });
511
+ }
512
+ }
513
+
514
+ const renderFnName = `nativ_${moduleId}_render`;
515
+ const propExtractions = props
516
+ .map((p) => {
517
+ if (p.type === "String")
518
+ return ` let ${p.name} = String(cString: nativ_jsi_get_string(runtime, props, "${p.name}"))`;
519
+ if (
520
+ p.type === "Double" ||
521
+ p.type === "Float" ||
522
+ p.type === "CGFloat"
523
+ )
524
+ return ` let ${p.name} = ${p.type}(nativ_jsi_get_number(runtime, props, "${p.name}"))`;
525
+ if (p.type === "Int")
526
+ return ` let ${p.name} = Int(nativ_jsi_get_number(runtime, props, "${p.name}"))`;
527
+ if (p.type === "Bool")
528
+ return ` let ${p.name} = nativ_jsi_has_prop(runtime, props, "${p.name}") != 0 && nativ_jsi_get_number(runtime, props, "${p.name}") != 0`;
529
+ if (p.type === "Color")
530
+ return ` let ${p.name}: Color = {
531
+ let hex = String(cString: nativ_jsi_get_string(runtime, props, "${p.name}"))
532
+ let scanner = Scanner(string: hex.hasPrefix("#") ? String(hex.dropFirst()) : hex)
533
+ var rgb: UInt64 = 0; scanner.scanHexInt64(&rgb)
534
+ return Color(red: Double((rgb >> 16) & 0xFF) / 255, green: Double((rgb >> 8) & 0xFF) / 255, blue: Double(rgb & 0xFF) / 255)
535
+ }()`;
536
+ return ` let ${p.name} = String(cString: nativ_jsi_get_string(runtime, props, "${p.name}")) // unsupported type: ${p.type}`;
537
+ })
538
+ .join("\n");
539
+
540
+ const initArgs = props
541
+ .map((p) => `${p.name}: ${p.name}`)
542
+ .join(", ");
543
+
544
+ const swiftCode = `// From: ${name}.swift
545
+ ${stripAnnotations(src)}
546
+
547
+ @_cdecl("${renderFnName}")
548
+ func ${renderFnName}(
549
+ _ view: UnsafeMutableRawPointer,
550
+ _ width: Float, _ height: Float,
551
+ _ runtime: UnsafeMutableRawPointer?,
552
+ _ props: UnsafeMutableRawPointer?
553
+ ) {
554
+ let parentView = Unmanaged<UIView>.fromOpaque(view).takeUnretainedValue()
555
+
556
+ ${propExtractions}
557
+
558
+ let swiftUIView = ${structName}(${initArgs})
559
+ let hostingController = UIHostingController(rootView: swiftUIView)
560
+ hostingController.view.frame = CGRect(x: 0, y: 0, width: CGFloat(width), height: CGFloat(height))
561
+ hostingController.view.backgroundColor = .clear
562
+
563
+ objc_setAssociatedObject(parentView, "nativHosting", hostingController, .OBJC_ASSOCIATION_RETAIN)
564
+ parentView.addSubview(hostingController.view)
565
+ }
566
+
567
+ // JSI C API — resolved at link time from NativFabric
568
+ @_silgen_name("nativ_jsi_get_string")
569
+ func nativ_jsi_get_string(_ rt: UnsafeMutableRawPointer?, _ obj: UnsafeMutableRawPointer?, _ name: UnsafePointer<CChar>) -> UnsafePointer<CChar>
570
+
571
+ @_silgen_name("nativ_jsi_get_number")
572
+ func nativ_jsi_get_number(_ rt: UnsafeMutableRawPointer?, _ obj: UnsafeMutableRawPointer?, _ name: UnsafePointer<CChar>) -> Double
573
+
574
+ @_silgen_name("nativ_jsi_has_prop")
575
+ func nativ_jsi_has_prop(_ rt: UnsafeMutableRawPointer?, _ obj: UnsafeMutableRawPointer?, _ name: UnsafePointer<CChar>) -> Int32
576
+ `;
577
+
578
+ return { swiftCode };
579
+ }
580
+
581
+ // ─── Helpers: Swift source handling ──────────────────────────────────
582
+
583
+ function stripAnnotations(src) {
584
+ return src
585
+ .replace(/\/\/\s*@nativ_export\s*\([^)]*\)\s*\n/g, "")
586
+ .replace(/\/\/\s*@nativ_component\s*\n/g, "");
587
+ }
588
+
589
+ // ─── Android: per-file C++ bridges (unchanged) ──────────────────────
590
+
591
+ function buildCppBridges() {
592
+ const exts = isIOS ? [".cpp", ".cc", ".mm"] : [".cpp", ".cc"];
593
+ const cppFiles = findUserFiles(exts);
594
+ if (cppFiles.length === 0) return;
595
+
596
+ for (const filepath of cppFiles) {
597
+ if (isCppComponent(filepath)) {
598
+ const baseName = path
599
+ .basename(filepath)
600
+ .replace(/\.(cpp|cc|mm)$/, "")
601
+ .toLowerCase();
602
+ const componentId = `nativ.${baseName}`;
603
+ const cppProps = extractCppComponentProps(filepath);
604
+ const propsTypeName = (() => {
605
+ const src = fs.readFileSync(filepath, "utf8");
606
+ const m = src.match(/NATIV_COMPONENT\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)/);
607
+ return m ? m[2] : null;
608
+ })();
609
+
610
+ const propExtractions = (cppProps || [])
611
+ .map((p) => {
612
+ if (p.cppType === "std::string")
613
+ return ` props.${p.name} = _nativ_get_string(rt, obj, "${p.jsName}", props.${p.name});`;
614
+ if (["double", "float", "int"].includes(p.cppType))
615
+ return ` props.${p.name} = _nativ_get_number(rt, obj, "${p.jsName}", props.${p.name});`;
616
+ if (p.cppType === "bool")
617
+ return ` props.${p.name} = _nativ_get_bool(rt, obj, "${p.jsName}", props.${p.name});`;
618
+ return "";
619
+ })
620
+ .join("\n");
621
+
622
+ const renderFnName = `nativ_${baseName}_render`;
623
+ const bridgeSrc = `
624
+ // Auto-generated production component bridge for ${baseName}
625
+ #include "${path.resolve(filepath)}"
626
+
627
+ extern "C"
628
+ void ${renderFnName}(void* view, float width, float height,
629
+ void* jsi_runtime, void* jsi_props) {
630
+ void* rt = jsi_runtime;
631
+ void* obj = jsi_props;
632
+ ${propsTypeName ? ` ${propsTypeName} props;\n${propExtractions}\n mount(view, width, height, props);` : " mount(view, width, height);"}
633
+ }
634
+
635
+ extern "C" {
636
+ typedef void (*NativRenderFn)(void*, float, float, void*, void*);
637
+ void nativ_register_render(const char*, NativRenderFn);
638
+ }
639
+
640
+ __attribute__((constructor, used))
641
+ static void register_${baseName}() {
642
+ nativ_register_render("${componentId}", ${renderFnName});
643
+ }
644
+ `;
645
+ const ext = filepath.endsWith(".mm") ? "mm" : "cpp";
646
+ fs.writeFileSync(
647
+ path.join(bridgeDir, `nativ_${baseName}_bridge.${ext}`),
648
+ bridgeSrc,
649
+ );
650
+ console.log(`[nativ] Bridge: ${baseName} (component)`);
651
+ } else {
652
+ const exports = extractCppExports(filepath, []);
653
+ if (exports.length === 0) continue;
654
+
655
+ const rel = path.relative(projectRoot, filepath);
656
+ const moduleId = rel
657
+ .replace(/\.(cpp|cc|mm)$/, "")
658
+ .replace(/[\/\\]/g, "_")
659
+ .replace(/[^a-zA-Z0-9_]/g, "_");
660
+ const bridgeSrc = generateBridge(exports, moduleId);
661
+ const userInclude = `#include "${path.resolve(filepath)}"\n\n`;
662
+ const ext = filepath.endsWith(".mm") ? "mm" : "cpp";
663
+ fs.writeFileSync(
664
+ path.join(bridgeDir, `${moduleId}_bridge.${ext}`),
665
+ userInclude + bridgeSrc,
666
+ );
667
+ console.log(`[nativ] Bridge: ${moduleId} (${exports.length} functions)`);
668
+ }
669
+ }
670
+ }
671
+
672
+ // ─── Rust: single unified static library ───────────────────────────────
673
+
674
+ function buildRustStatic() {
675
+ const rsFiles = findUserFiles([".rs"]);
676
+ if (rsFiles.length === 0) return;
677
+
678
+ const abiTargets = isAndroid
679
+ ? [
680
+ {
681
+ abi: "arm64-v8a",
682
+ rust: "aarch64-linux-android",
683
+ linkerPrefix: "aarch64-linux-android",
684
+ },
685
+ {
686
+ abi: "armeabi-v7a",
687
+ rust: "armv7-linux-androideabi",
688
+ linkerPrefix: "armv7a-linux-androideabi",
689
+ },
690
+ {
691
+ abi: "x86_64",
692
+ rust: "x86_64-linux-android",
693
+ linkerPrefix: "x86_64-linux-android",
694
+ },
695
+ ]
696
+ : [{ abi: "arm64", rust: "aarch64-apple-ios" }];
697
+
698
+ // Check if all outputs are up to date
699
+ const outputPaths = abiTargets.map((t) => {
700
+ const dir = isAndroid ? path.join(releaseDir, t.abi) : releaseDir;
701
+ return path.join(dir, "libnativ_user.a");
702
+ });
703
+ const allUpToDate = outputPaths.every((p) => {
704
+ if (!fs.existsSync(p)) return false;
705
+ const stat = fs.statSync(p);
706
+ if (stat.size < 100) return false; // stub archive — must rebuild
707
+ return rsFiles.every((f) => fs.statSync(f).mtimeMs < stat.mtimeMs);
708
+ });
709
+ if (allUpToDate) {
710
+ console.log(`[nativ] Rust: all targets up to date, skipping`);
711
+ return;
712
+ }
713
+
714
+ const {
715
+ generateFunctionWrapper,
716
+ generateComponentWrapper,
717
+ } = require("./rust-compiler");
718
+ const {
719
+ extractRustExports: _extractRust,
720
+ } = require("../extractors/rust-extractor");
721
+
722
+ const buildBase = path.join(projectRoot, ".nativ/build");
723
+ const unifiedDir = path.join(buildBase, "nativ_unified");
724
+ fs.mkdirSync(path.join(unifiedDir, "src"), { recursive: true });
725
+
726
+ // Forward deps from root Cargo.toml
727
+ let rootDeps = "";
728
+ try {
729
+ const rootToml = fs.readFileSync(
730
+ path.join(projectRoot, "Cargo.toml"),
731
+ "utf8",
732
+ );
733
+ const depsSection = rootToml.match(
734
+ /\[dependencies\]([\s\S]*?)(?:\n\[|\n*$)/,
735
+ );
736
+ if (depsSection) {
737
+ rootDeps = depsSection[1].replace(/path\s*=\s*"([^"]+)"/g, (_, p) => {
738
+ const absPath = path.resolve(projectRoot, p);
739
+ const relPath = path.relative(unifiedDir, absPath);
740
+ return `path = "${relPath}"`;
741
+ });
742
+ }
743
+ } catch {}
744
+
745
+ fs.writeFileSync(
746
+ path.join(unifiedDir, "Cargo.toml"),
747
+ `[package]
748
+ name = "nativ-user"
749
+ version = "0.1.0"
750
+ edition = "2024"
751
+
752
+ [lib]
753
+ crate-type = ["staticlib"]
754
+
755
+ [workspace]
756
+
757
+ [dependencies]
758
+ ${rootDeps}
759
+
760
+ [profile.release]
761
+ opt-level = "z"
762
+ lto = true
763
+ `,
764
+ );
765
+
766
+ const modules = [];
767
+ for (const filepath of rsFiles) {
768
+ const { functions, isComponent } = _extractRust(filepath);
769
+ if (!isComponent && functions.length === 0) continue;
770
+
771
+ const name = path.basename(filepath, ".rs").toLowerCase();
772
+ const userSrc = fs.readFileSync(filepath, "utf8");
773
+
774
+ let moduleSrc;
775
+ if (isComponent) {
776
+ moduleSrc = generateComponentWrapper(userSrc, name, { unified: true });
777
+ } else {
778
+ moduleSrc = generateFunctionWrapper(userSrc, functions, name, {
779
+ unified: true,
780
+ });
781
+ }
782
+ fs.writeFileSync(path.join(unifiedDir, "src", `${name}.rs`), moduleSrc);
783
+ modules.push(name);
784
+ }
785
+
786
+ if (modules.length === 0) {
787
+ console.log("[nativ] Rust: no exported modules found");
788
+ return;
789
+ }
790
+
791
+ const libRs = [
792
+ "// Auto-generated by React Native Native — do not edit",
793
+ "#![allow(unused, non_snake_case, unused_unsafe)]",
794
+ "",
795
+ "pub use nativ_core::prelude::*;",
796
+ "",
797
+ ...modules.map((m) => `pub mod ${m};`),
798
+ "",
799
+ ].join("\n");
800
+ fs.writeFileSync(path.join(unifiedDir, "src/lib.rs"), libRs);
801
+
802
+ const sharedTarget = path.join(buildBase, "cargo-target");
803
+
804
+ // Resolve NDK toolchain for Android
805
+ let ndkBinDir = null;
806
+ if (isAndroid) {
807
+ const androidHome =
808
+ process.env.ANDROID_HOME ||
809
+ path.join(process.env.HOME, "Library/Android/sdk");
810
+ const ndkDir = path.join(androidHome, "ndk");
811
+ try {
812
+ const versions = fs.readdirSync(ndkDir).sort();
813
+ if (versions.length > 0) {
814
+ const toolchain = path.join(
815
+ ndkDir,
816
+ versions[versions.length - 1],
817
+ "toolchains/llvm/prebuilt",
818
+ );
819
+ const hosts = fs.readdirSync(toolchain);
820
+ if (hosts.length > 0)
821
+ ndkBinDir = path.join(toolchain, hosts[0], "bin");
822
+ }
823
+ } catch {}
824
+ if (!ndkBinDir) {
825
+ console.error(
826
+ "[nativ] Android NDK not found — cannot build Rust for Android",
827
+ );
828
+ return;
829
+ }
830
+ }
831
+
832
+ for (const { abi, rust: target, linkerPrefix } of abiTargets) {
833
+ const outDir = isAndroid ? path.join(releaseDir, abi) : releaseDir;
834
+ fs.mkdirSync(outDir, { recursive: true });
835
+ const outputLib = path.join(outDir, "libnativ_user.a");
836
+
837
+ const home = process.env.HOME || require("os").homedir();
838
+ const cargoPath = fs.existsSync(path.join(home, ".cargo/bin/cargo"))
839
+ ? path.join(home, ".cargo/bin/cargo")
840
+ : "cargo";
841
+
842
+ const cmd = [
843
+ cargoPath,
844
+ "build",
845
+ "--release",
846
+ "--manifest-path",
847
+ path.join(unifiedDir, "Cargo.toml"),
848
+ `--target=${target}`,
849
+ "--lib",
850
+ ];
851
+
852
+ const env = { ...process.env, CARGO_TARGET_DIR: sharedTarget };
853
+ if (isIOS) {
854
+ env.RUSTFLAGS =
855
+ "--cfg unified -C link-arg=-undefined -C link-arg=dynamic_lookup";
856
+ }
857
+ if (isAndroid) {
858
+ const envKey = `CARGO_TARGET_${target.toUpperCase().replace(/-/g, "_")}_LINKER`;
859
+ env[envKey] = path.join(ndkBinDir, `${linkerPrefix}24-clang`);
860
+ env.RUSTFLAGS = "--cfg unified -C link-arg=-llog";
861
+ }
862
+
863
+ console.log(
864
+ `[nativ] Compiling Rust (${modules.length} modules, ${abi} → ${target})...`,
865
+ );
866
+ try {
867
+ execSync(cmd.join(" "), { stdio: "pipe", encoding: "utf8", env });
868
+ } catch (err) {
869
+ console.error(`[nativ] Rust compile failed for ${abi}`);
870
+ console.error((err.stderr || "").slice(0, 3000));
871
+ continue;
872
+ }
873
+
874
+ const builtLib = path.join(sharedTarget, target, "release/libnativ_user.a");
875
+ if (fs.existsSync(builtLib)) {
876
+ fs.copyFileSync(builtLib, outputLib);
877
+ const size = fs.statSync(outputLib).size;
878
+ console.log(
879
+ `[nativ] Built ${abi}/libnativ_user.a (${(size / 1024).toFixed(1)}KB)`,
880
+ );
881
+ } else {
882
+ console.error(`[nativ] libnativ_user.a not found at ${builtLib}`);
883
+ }
884
+ }
885
+ }
886
+
887
+ // ─── Kotlin wrapper generation (Android only) ──────────────────────────
888
+
889
+ function buildKotlinSources() {
890
+ if (!isAndroid) return [];
891
+ const ktFiles = findUserFiles([".kt"]);
892
+ if (ktFiles.length === 0) return [];
893
+
894
+ const ktSrcDir = path.join(genDir, "kotlin-src/com/nativfabric/generated");
895
+ fs.mkdirSync(ktSrcDir, { recursive: true });
896
+ const registeredModules = [];
897
+
898
+ for (const filepath of ktFiles) {
899
+ const { functions, isComponent } = extractKotlinExports(filepath);
900
+ const baseName = path.basename(filepath, ".kt");
901
+ const moduleId = baseName.toLowerCase();
902
+ const className = `NativModule_${moduleId}`;
903
+ const userSrc = fs.readFileSync(filepath, "utf8");
904
+
905
+ if (isComponent) {
906
+ const cleanSrc = userSrc
907
+ .replace(/\/\/\s*@nativ_component\s*\n/g, "")
908
+ .replace(/^package\s+[^\n]+\n/m, "")
909
+ .replace(/^import\s+[^\n]+\n/gm, "");
910
+ const userImports = [...userSrc.matchAll(/^(import\s+[^\n]+)\n/gm)].map(
911
+ (m) => m[1],
912
+ );
913
+ const isCompose = userSrc.includes("@Composable");
914
+
915
+ if (isCompose) {
916
+ const compFnMatch = cleanSrc.match(
917
+ /@Composable\s+fun\s+(\w+)\s*\(([^)]*)\)/,
918
+ );
919
+ const compFnName = compFnMatch ? compFnMatch[1] : baseName;
920
+ const compParams =
921
+ compFnMatch && compFnMatch[2] ? compFnMatch[2].trim() : "";
922
+
923
+ let compCall;
924
+ if (compParams) {
925
+ const compArgs = compParams
926
+ .split(",")
927
+ .map((p) => p.trim())
928
+ .filter(Boolean);
929
+ const argExprs = compArgs
930
+ .map((p) => {
931
+ const m = p.match(/(\w+)\s*:\s*(.+)/);
932
+ if (!m) return null;
933
+ const [, pName, pType] = m;
934
+ const t = pType.trim();
935
+ if (t === "String")
936
+ return ` ${pName} = props["${pName}"] as? String ?: ""`;
937
+ if (t === "Int")
938
+ return ` ${pName} = (props["${pName}"] as? Number)?.toInt() ?: 0`;
939
+ if (t === "Float" || t === "Double")
940
+ return ` ${pName} = (props["${pName}"] as? Number)?.toDouble() ?: 0.0`;
941
+ if (t === "Boolean")
942
+ return ` ${pName} = props["${pName}"] as? Boolean ?: false`;
943
+ if (t.includes("->"))
944
+ return ` ${pName} = {}`;
945
+ return ` ${pName} = props["${pName}"]`;
946
+ })
947
+ .filter(Boolean);
948
+ compCall = ` ${compFnName}(\n${argExprs.join(",\n")}\n )`;
949
+ } else {
950
+ compCall = ` ${compFnName}()`;
951
+ }
952
+
953
+ const wrapper = [
954
+ `package com.nativfabric.generated`,
955
+ "",
956
+ "import android.view.ViewGroup",
957
+ "import android.widget.FrameLayout",
958
+ "import androidx.compose.runtime.*",
959
+ "import androidx.compose.ui.platform.ComposeView",
960
+ ...userImports,
961
+ "",
962
+ cleanSrc,
963
+ "",
964
+ `object ${className} {`,
965
+ " @JvmStatic",
966
+ " fun render(parent: ViewGroup, props: Map<String, Any?>) {",
967
+ " val composeView = ComposeView(parent.context)",
968
+ " composeView.setContent {",
969
+ compCall,
970
+ " }",
971
+ " parent.addView(composeView, FrameLayout.LayoutParams(",
972
+ " FrameLayout.LayoutParams.MATCH_PARENT,",
973
+ " FrameLayout.LayoutParams.MATCH_PARENT))",
974
+ " }",
975
+ "}",
976
+ ].join("\n");
977
+ fs.writeFileSync(path.join(ktSrcDir, `${className}.kt`), wrapper);
978
+ } else {
979
+ const fnMatch = cleanSrc.match(/fun\s+(\w+)\s*\(\s*parent\s*:/);
980
+ const fnName = fnMatch ? fnMatch[1] : baseName;
981
+ const wrapper = [
982
+ `package com.nativfabric.generated`,
983
+ "",
984
+ ...userImports,
985
+ "",
986
+ cleanSrc,
987
+ "",
988
+ `object ${className} {`,
989
+ " @JvmStatic",
990
+ " fun render(parent: android.view.ViewGroup, props: Map<String, Any?>) {",
991
+ ` ${fnName}(parent, props)`,
992
+ " }",
993
+ "}",
994
+ ].join("\n");
995
+ fs.writeFileSync(path.join(ktSrcDir, `${className}.kt`), wrapper);
996
+ }
997
+ registeredModules.push(moduleId);
998
+ console.log(`[nativ] Kotlin wrapper: ${moduleId} (component)`);
999
+ } else if (functions.length > 0) {
1000
+ const cleanSrc = userSrc
1001
+ .replace(/\/\/\s*@nativ_export\s*\([^)]*\)\s*\n/g, "")
1002
+ .replace(/^package\s+[^\n]+\n/m, "");
1003
+
1004
+ const lines = [
1005
+ `package com.nativfabric.generated`,
1006
+ "",
1007
+ cleanSrc,
1008
+ "",
1009
+ `private fun _parseArgs(json: String): List<String> {`,
1010
+ ` val s = json.trim().removePrefix("[").removeSuffix("]")`,
1011
+ ` if (s.isBlank()) return emptyList()`,
1012
+ ` val result = mutableListOf<String>()`,
1013
+ ` var i = 0; var inStr = false; var esc = false; val buf = StringBuilder()`,
1014
+ ` while (i < s.length) { val c = s[i]; when {`,
1015
+ ` esc -> { buf.append(c); esc = false }; c == '\\\\' -> esc = true`,
1016
+ ` c == '"' -> inStr = !inStr; c == ',' && !inStr -> { result.add(buf.toString().trim()); buf.clear() }`,
1017
+ ` else -> buf.append(c) }; i++ }`,
1018
+ ` if (buf.isNotEmpty()) result.add(buf.toString().trim()); return result }`,
1019
+ "",
1020
+ `object ${className} {`,
1021
+ " @JvmStatic",
1022
+ " fun dispatch(fnName: String, argsJson: String): String {",
1023
+ " val args = _parseArgs(argsJson)",
1024
+ " return when (fnName) {",
1025
+ ];
1026
+ for (const fn of functions) {
1027
+ const argExprs = fn.args.map((a, i) => {
1028
+ switch (a.type) {
1029
+ case "Int":
1030
+ return `args[${i}].toInt()`;
1031
+ case "Long":
1032
+ return `args[${i}].toLong()`;
1033
+ case "Float":
1034
+ return `args[${i}].toFloat()`;
1035
+ case "Double":
1036
+ return `args[${i}].toDouble()`;
1037
+ case "Boolean":
1038
+ return `args[${i}].toBoolean()`;
1039
+ default:
1040
+ return `args[${i}]`;
1041
+ }
1042
+ });
1043
+ const call = `${fn.name}(${argExprs.join(", ")})`;
1044
+ if (fn.ret === "String")
1045
+ lines.push(` "${fn.name}" -> "\\"" + ${call} + "\\""`);
1046
+ else if (fn.ret === "Unit")
1047
+ lines.push(` "${fn.name}" -> { ${call}; "null" }`);
1048
+ else lines.push(` "${fn.name}" -> ${call}.toString()`);
1049
+ }
1050
+ lines.push(' else -> "null"', " }", " }", "}");
1051
+ fs.writeFileSync(
1052
+ path.join(ktSrcDir, `${className}.kt`),
1053
+ lines.join("\n"),
1054
+ );
1055
+ registeredModules.push(moduleId);
1056
+ console.log(
1057
+ `[nativ] Kotlin wrapper: ${moduleId} (${functions.length} functions)`,
1058
+ );
1059
+ }
1060
+ }
1061
+ return registeredModules;
1062
+ }
1063
+
1064
+ // ─── Run ───────────────────────────────────────────────────────────────
1065
+
1066
+ const t0 = Date.now();
1067
+
1068
+ if (isIOS) {
1069
+ buildCombinedIOSBridges();
1070
+ buildRustStatic();
1071
+ } else {
1072
+ buildCppBridges();
1073
+ buildRustStatic();
1074
+ const kotlinModules = buildKotlinSources();
1075
+
1076
+ // Kotlin registry
1077
+ if (kotlinModules && kotlinModules.length > 0) {
1078
+ const ktSrcDir = path.join(
1079
+ genDir,
1080
+ "kotlin-src/com/nativfabric/generated",
1081
+ );
1082
+ const registrations = kotlinModules
1083
+ .map((moduleId) => {
1084
+ const className = `NativModule_${moduleId}`;
1085
+ return ` try {
1086
+ val clazz = Class.forName("com.nativfabric.generated.${className}")
1087
+ try {
1088
+ val dispatch = clazz.getMethod("dispatch", String::class.java, String::class.java)
1089
+ com.nativfabric.NativRuntime.registerKotlinDispatch("${moduleId}", dispatch)
1090
+ } catch (_: NoSuchMethodException) {}
1091
+ try {
1092
+ val render = clazz.getMethod("render", android.view.ViewGroup::class.java, Map::class.java)
1093
+ com.nativfabric.NativRuntime.registerKotlinRenderer("nativ.${moduleId}", render)
1094
+ } catch (_: NoSuchMethodException) {}
1095
+ } catch (e: Exception) {
1096
+ android.util.Log.w("NativRegistry", "Module ${moduleId}: \${e.message}")
1097
+ }`;
1098
+ })
1099
+ .join("\n");
1100
+
1101
+ fs.writeFileSync(
1102
+ path.join(ktSrcDir, "NativModuleRegistry.kt"),
1103
+ `package com.nativfabric.generated
1104
+
1105
+ object NativModuleRegistry {
1106
+ init {
1107
+ ${registrations}
1108
+ }
1109
+
1110
+ fun ensure() {} // Called to trigger class loading
1111
+ }
1112
+ `,
1113
+ );
1114
+ console.log(`[nativ] Kotlin registry: ${kotlinModules.length} modules`);
1115
+ }
1116
+ }
1117
+
1118
+ console.log(`[nativ] Static compilation done in ${Date.now() - t0}ms`);