@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,649 @@
1
+ /**
2
+ * Universal Nativ transformer — routes .rs, .cpp, .mm, .cc files
3
+ * to the appropriate handler. All other files go to the default Expo
4
+ * Babel transformer.
5
+ */
6
+
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+ const { extractCppExports } = require('./extractors/cpp-ast-extractor');
10
+ const { extractRustExports } = require('./extractors/rust-extractor');
11
+ const { generateDTS } = require('./utils/dts-generator');
12
+ const { getIncludePaths } = require('./utils/include-resolver');
13
+ const { generateCompileCommands } = require('./utils/compile-commands');
14
+ const { compileDylib, compileCppComponentDylib } = require('./compilers/dylib-compiler');
15
+ const { compileRustDylib } = require('./compilers/rust-compiler');
16
+ const { compileAndroidCppDylib, compileAndroidCppComponentDylib, compileAndroidRustDylib } = require('./compilers/android-compiler');
17
+ const { compileKotlinDex, extractKotlinExports } = require('./compilers/kotlin-compiler');
18
+
19
+ // Resolve the default Expo transformer. Since this file lives in the package
20
+ // (not the app), we resolve from process.cwd() which is the app root.
21
+ let upstreamTransformer;
22
+ try {
23
+ const mc = require(require.resolve('expo/metro-config', { paths: [process.cwd()] }));
24
+ const cfg = mc.getDefaultConfig(process.cwd());
25
+ upstreamTransformer = require(cfg.transformer.babelTransformerPath);
26
+ } catch {
27
+ upstreamTransformer = require(require.resolve(
28
+ '@expo/metro-config/babel-transformer',
29
+ { paths: [process.cwd()] }
30
+ ));
31
+ }
32
+
33
+ // Cached per Metro session
34
+ let _includePaths = null;
35
+ let _buildCounter = 0;
36
+
37
+ // ─── Rust component shim ──────────────────────────────────────────────
38
+
39
+ function componentIdForFile(filename) {
40
+ const name = path.basename(filename, '.rs').toLowerCase();
41
+ return `nativ.${name}`;
42
+ }
43
+
44
+ function rustComponentShim(componentId, srcHash, libExt) {
45
+ const displayName = componentId.split('.').pop();
46
+ const moduleId = componentId.split('.').pop().toLowerCase();
47
+ const _ext = libExt || 'dylib';
48
+ // Fast Refresh swaps function bodies but keeps module-level vars.
49
+ // So we put the hash as a literal inside the function body — when FR
50
+ // patches the function, the new hash is baked into the new body.
51
+ return `
52
+ import React from 'react';
53
+ import NativContainer from '@react-native-native/nativ-fabric/src/NativContainerNativeComponent';
54
+
55
+ if (!global.__nativ_loaded) global.__nativ_loaded = {};
56
+
57
+ const ${displayName} = React.forwardRef((props, ref) => {
58
+ const { style, children, ...nativeProps } = props;
59
+
60
+ // Hot-reload: load dylib when source changes
61
+ const hash = '${srcHash}';
62
+ if (global.__nativ_loaded['${moduleId}'] !== hash) {
63
+ global.__nativ_loaded['${moduleId}'] = hash;
64
+ try {
65
+ const { NativeModules } = require('react-native');
66
+ const _scriptUrl = NativeModules?.SourceCode?.getConstants?.()?.scriptURL || '';
67
+ const _host = _scriptUrl.match(/^https?:\\/\\/[^/]+/)?.[0] || '';
68
+ if (_host && global.__nativ?.loadDylib) {
69
+ const _t = global.__nativ.target || '';
70
+ global.__nativ.loadDylib(_host + '/__nativ_dylib/' + _t + '/nativ_${moduleId}_' + hash + '.${_ext}');
71
+ }
72
+ } catch (e) {
73
+ if (typeof __DEV__ !== 'undefined' && __DEV__) console.log('[nativ] dylib load:', e?.message);
74
+ }
75
+ }
76
+
77
+ // Store props in JSI — called on every render (React handles when to re-render)
78
+ if (global.__nativ?.setComponentProps) {
79
+ global.__nativ.setComponentProps('${componentId}', nativeProps);
80
+ }
81
+
82
+ // DEBUG: use Date.now() to guarantee unique propsJson every render
83
+ // If numbers STILL stop, the issue is not in propsJson comparison
84
+ const _pj = String(Date.now());
85
+
86
+ return (
87
+ <NativContainer
88
+ style={style}
89
+ ref={ref}
90
+ key={'${srcHash}'}
91
+ componentId="${componentId}"
92
+ propsJson={_pj}
93
+ />
94
+ );
95
+ });
96
+ ${displayName}.displayName = '${displayName}';
97
+
98
+ export default ${displayName};
99
+ export { ${displayName} };
100
+ `;
101
+ }
102
+
103
+ // ─── Production shims ─────────────────────────────────────────────────
104
+ // In release builds (dev: false), all native code is statically linked.
105
+ // No loadDylib, no __nativ_loaded tracking, no Metro host URL.
106
+ // Functions are already registered at app start via constructors.
107
+
108
+ function cppFunctionShimProd(exports, moduleId) {
109
+ const lines = [`import '@react-native-native/nativ-fabric';`, ''];
110
+ for (const fn of exports) {
111
+ const argNames = fn.args.map(a => a.name).join(', ');
112
+ if (fn.async) {
113
+ lines.push(
114
+ `export function ${fn.name}(${argNames}) {`,
115
+ ` const argsJson = JSON.stringify([${argNames}]);`,
116
+ ` return global.__nativ.callAsync('${moduleId}', '${fn.name}', argsJson);`,
117
+ `}`,
118
+ );
119
+ } else {
120
+ lines.push(
121
+ `export function ${fn.name}(${argNames}) {`,
122
+ ` const argsJson = JSON.stringify([${argNames}]);`,
123
+ ` const result = global.__nativ.callSync('${moduleId}', '${fn.name}', argsJson);`,
124
+ ` return JSON.parse(result);`,
125
+ `}`,
126
+ );
127
+ }
128
+ lines.push('');
129
+ }
130
+ return lines.join('\n');
131
+ }
132
+
133
+ function rustComponentShimProd(componentId) {
134
+ const displayName = componentId.split('.').pop();
135
+ return `
136
+ import React from 'react';
137
+ import NativContainer from '@react-native-native/nativ-fabric/src/NativContainerNativeComponent';
138
+
139
+ const ${displayName} = React.forwardRef((props, ref) => {
140
+ const { style, children, ...nativeProps } = props;
141
+
142
+ if (global.__nativ?.setComponentProps) {
143
+ global.__nativ.setComponentProps('${componentId}', nativeProps);
144
+ }
145
+
146
+ return (
147
+ <NativContainer
148
+ style={style}
149
+ ref={ref}
150
+ componentId="${componentId}"
151
+ propsJson={JSON.stringify(nativeProps)}
152
+ />
153
+ );
154
+ });
155
+ ${displayName}.displayName = '${displayName}';
156
+
157
+ export default ${displayName};
158
+ export { ${displayName} };
159
+ `;
160
+ }
161
+
162
+ // ─── C++/ObjC++ function shim ─────────────────────────────────────────
163
+
164
+ function moduleIdForFile(filename, projectRoot) {
165
+ const rel = path.relative(projectRoot, filename);
166
+ return rel
167
+ .replace(/\.(cpp|cc|mm|c)$/, '')
168
+ .replace(/[\/\\]/g, '_')
169
+ .replace(/[^a-zA-Z0-9_]/g, '_');
170
+ }
171
+
172
+ function cppFunctionShim(exports, moduleId, srcHash, dylibId, libExt) {
173
+ const _dylibId = dylibId || moduleId;
174
+ const _ext = libExt || 'dylib';
175
+ // Ensure @react-native-native/nativ-fabric is imported to trigger TurboModule load → installJSIBindings → global.__nativ
176
+ // Fast Refresh only swaps function bodies — so the hash check and dylib load
177
+ // must be INSIDE each exported function as string literals.
178
+ // global.__nativ_loaded tracks which version is loaded per module.
179
+ const loadSnippet = [
180
+ ` if (!global.__nativ_loaded) global.__nativ_loaded = {};`,
181
+ ` if (global.__nativ_loaded['${_dylibId}'] !== '${srcHash}') {`,
182
+ ` global.__nativ_loaded['${_dylibId}'] = '${srcHash}';`,
183
+ ` try {`,
184
+ ` var _s = require('react-native').NativeModules?.SourceCode?.getConstants?.()?.scriptURL || '';`,
185
+ ` var _h = (_s.match(/^https?:\\/\\/[^/]+/) || [''])[0];`,
186
+ ` var _t = global.__nativ?.target || '';`,
187
+ ` if (_h && global.__nativ?.loadDylib) {`,
188
+ ` global.__nativ.loadDylib(_h + '/__nativ_dylib/' + _t + '/${_dylibId}_${srcHash}.${_ext}');`,
189
+ ` }`,
190
+ ` } catch(e) {}`,
191
+ ` }`,
192
+ ].join('\n');
193
+
194
+ const lines = [`import '@react-native-native/nativ-fabric';`, ''];
195
+
196
+ for (const fn of exports) {
197
+ const argNames = fn.args.map(a => a.name).join(', ');
198
+
199
+ if (fn.async) {
200
+ lines.push(
201
+ `export function ${fn.name}(${argNames}) {`,
202
+ loadSnippet,
203
+ ` const argsJson = JSON.stringify([${argNames}]);`,
204
+ ` return global.__nativ.callAsync('${moduleId}', '${fn.name}', argsJson);`,
205
+ `}`,
206
+ );
207
+ } else {
208
+ lines.push(
209
+ `export function ${fn.name}(${argNames}) {`,
210
+ loadSnippet,
211
+ ` const argsJson = JSON.stringify([${argNames}]);`,
212
+ ` const result = global.__nativ.callSync('${moduleId}', '${fn.name}', argsJson);`,
213
+ ` return JSON.parse(result);`,
214
+ `}`,
215
+ );
216
+ }
217
+ lines.push('');
218
+ }
219
+
220
+ return lines.join('\n');
221
+ }
222
+
223
+ // ─── Transform entry point ────────────────────────────────────────────
224
+
225
+ // Content-addressed caching: same source → same hash → same JS shim.
226
+ // Metro's SHA1 cache naturally deduplicates. Undo (A→B→A) serves the
227
+ // cached shim for A, which references the correct binary (also hashed).
228
+ const _sessionId = Date.now().toString(36);
229
+ module.exports.getCacheKey = function () {
230
+ return `nativ-transformer-${_sessionId}`;
231
+ };
232
+
233
+ module.exports.transform = async function nativTransform({
234
+ filename,
235
+ src,
236
+ options,
237
+ ...rest
238
+ }) {
239
+ const projectRoot = options.projectRoot;
240
+ const platform = options.platform || 'ios';
241
+ const isAndroid = platform === 'android';
242
+ const isDev = options.dev !== false;
243
+
244
+ // Read last-known target (written at startup + updated by middleware on device switch)
245
+ let buildTarget;
246
+ try {
247
+ const targetFile = path.join(projectRoot, `.nativ/${isAndroid ? 'android' : 'ios'}-target`);
248
+ buildTarget = fs.readFileSync(targetFile, 'utf8').trim();
249
+ } catch {}
250
+ if (!buildTarget) buildTarget = isAndroid ? 'arm64-v8a' : 'device';
251
+
252
+ const isNative = filename.startsWith(projectRoot) &&
253
+ !filename.includes('node_modules') &&
254
+ !filename.includes('/packages/') &&
255
+ (filename.endsWith('.rs') || filename.endsWith('.cpp') ||
256
+ filename.endsWith('.cc') || filename.endsWith('.mm') ||
257
+ filename.endsWith('.swift') || filename.endsWith('.kt'));
258
+
259
+ // Skip platform-incompatible files (but still generate .d.ts for IDE)
260
+ if (isAndroid && (filename.endsWith('.swift') || filename.endsWith('.mm'))) {
261
+ if (isDev && filename.endsWith('.swift')) {
262
+ try {
263
+ const { extractSwiftExports } = require('./compilers/swift-compiler');
264
+ const swExports = extractSwiftExports(filename);
265
+ if (swExports.length > 0) {
266
+ const lines = ['// Auto-generated by React Native Native', ''];
267
+ for (const fn of swExports) {
268
+ lines.push(`export declare function ${fn.name}(): ${fn.async ? 'Promise<string>' : 'string'};`);
269
+ }
270
+ lines.push('');
271
+ fs.writeFileSync(dtsPath(filename), lines.join('\n'));
272
+ }
273
+ } catch {}
274
+ }
275
+ return upstreamTransformer.transform({
276
+ filename: filename.replace(/\.(swift|mm)$/, '.js'),
277
+ src: 'export default undefined;\n',
278
+ options, ...rest,
279
+ });
280
+ }
281
+ if (!isAndroid && filename.endsWith('.kt')) {
282
+ // Still generate .d.ts for IDE support even on iOS
283
+ if (isDev) {
284
+ try {
285
+ const { functions: ktFns, isComponent: ktIsComp, componentProps: ktProps } = extractKotlinExports(filename);
286
+ const ktBaseName = path.basename(filename, '.kt');
287
+ if (ktIsComp) {
288
+ const propsLines = ktProps.filter(p => !p.isCallback).map(p => ` ${p.jsName}?: ${p.tsType};`);
289
+ const cbLines = ktProps.filter(p => p.isCallback).map(p => ` ${p.jsName}?: () => void;`);
290
+ fs.writeFileSync(dtsPath(filename), [
291
+ `import type { ViewProps } from 'react-native';`, '',
292
+ `interface ${ktBaseName}Props extends ViewProps {`, ...propsLines, ...cbLines, `}`, '',
293
+ `declare const ${ktBaseName}: React.ComponentType<${ktBaseName}Props>;`,
294
+ `export default ${ktBaseName};`, `export { ${ktBaseName} };`, '',
295
+ ].join('\n'));
296
+ } else if (ktFns.length > 0) {
297
+ const lines = ['// Auto-generated by React Native Native', ''];
298
+ for (const fn of ktFns) {
299
+ const args = fn.args.map(a => `${a.name}: ${a.tsType}`).join(', ');
300
+ lines.push(`export declare function ${fn.name}(${args}): ${fn.tsType};`);
301
+ }
302
+ lines.push('');
303
+ fs.writeFileSync(dtsPath(filename), lines.join('\n'));
304
+ }
305
+ } catch {}
306
+ }
307
+ return upstreamTransformer.transform({
308
+ filename: filename.replace(/\.kt$/, '.js'),
309
+ src: 'export default undefined;\n',
310
+ options, ...rest,
311
+ });
312
+ }
313
+
314
+ if (isNative) {
315
+ console.log(`[nativ] transform (${platform}): ${path.basename(filename)} (build #${++_buildCounter})`);
316
+ }
317
+
318
+ // ── .d.ts output path ────────────────────────────────────────────────
319
+ // Write typings to .nativ/typings/, stripping native extension.
320
+ // tsconfig rootDirs: [".", ".nativ/typings"] makes TS find them.
321
+ // e.g. math_utils.cpp → .nativ/typings/math_utils.d.ts
322
+ function dtsPath(sourceFile) {
323
+ const rel = path.relative(projectRoot, sourceFile);
324
+ const dtsRel = rel.replace(/\.(rs|cpp|cc|mm|c|swift|kt)$/, '.d.ts');
325
+ const out = path.join(projectRoot, '.nativ/typings', dtsRel);
326
+ fs.mkdirSync(path.dirname(out), { recursive: true });
327
+ return out;
328
+ }
329
+
330
+ // ── Content-addressed compile + manifest ─────────────────────────────
331
+ // Compiles for the last-known target (single flash hot-reload).
332
+ // Also writes manifest so the middleware can compile on-demand for other targets.
333
+ const manifestPath = path.join(projectRoot, '.nativ/modules.json');
334
+ function writeManifest(dylibName, entry) {
335
+ let manifest = {};
336
+ try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch {}
337
+ manifest[dylibName] = entry;
338
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
339
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
340
+ }
341
+
342
+ function cachedCompile(srcContent, origName, ext, compileFn) {
343
+ const hash = require('crypto').createHash('md5').update(srcContent).digest('hex').slice(0, 8);
344
+ const hashedName = `${origName}_${hash}.${ext}`;
345
+ const dylibDir = path.join(projectRoot, '.nativ/dylibs', buildTarget);
346
+ fs.mkdirSync(dylibDir, { recursive: true });
347
+ const hashedPath = path.join(dylibDir, hashedName);
348
+ if (fs.existsSync(hashedPath)) {
349
+ console.log(`[nativ] ${origName} cache hit (${hash}, ${buildTarget})`);
350
+ return hash;
351
+ }
352
+ compileFn(); // compile for last-known target
353
+ const origPath = path.join(dylibDir, `${origName}.${ext}`);
354
+ if (fs.existsSync(origPath)) {
355
+ try { fs.copyFileSync(origPath, hashedPath); } catch {}
356
+ }
357
+ return hash;
358
+ }
359
+
360
+ // ── Rust files → component or function shim
361
+ if (filename.endsWith('.rs')) {
362
+ const { functions, isComponent, componentProps } = extractRustExports(filename);
363
+ const baseName = path.basename(filename, '.rs').toLowerCase();
364
+
365
+ let srcHash = 'prod';
366
+ if (isDev) {
367
+ const libExt = isAndroid ? 'so' : 'dylib';
368
+ srcHash = cachedCompile(src, `nativ_${baseName}`, libExt, () => {
369
+ if (isAndroid) compileAndroidRustDylib(filename, projectRoot, { target: buildTarget });
370
+ else compileRustDylib(filename, projectRoot, { target: buildTarget });
371
+ });
372
+ writeManifest(`nativ_${baseName}`, { source: filename, type: isComponent ? 'rust-component' : 'rust' });
373
+ // Generate .d.ts for TypeScript support
374
+ try {
375
+ if (isComponent) {
376
+ const displayName = path.basename(filename, '.rs');
377
+ const propsLines = componentProps.map(p => ` ${p.jsName}?: ${p.tsType};`);
378
+ const dts = [
379
+ `import type { ViewProps } from 'react-native';`,
380
+ ``,
381
+ `interface ${displayName}Props extends ViewProps {`,
382
+ ...propsLines,
383
+ `}`,
384
+ ``,
385
+ `declare const ${displayName}: React.ComponentType<${displayName}Props>;`,
386
+ `export default ${displayName};`,
387
+ `export { ${displayName} };`,
388
+ ``,
389
+ ].join('\n');
390
+ fs.writeFileSync(dtsPath(filename), dts);
391
+ } else if (functions.length > 0) {
392
+ fs.writeFileSync(dtsPath(filename), generateDTS(functions));
393
+ }
394
+ } catch {}
395
+ }
396
+
397
+ const _libExt = isAndroid ? 'so' : 'dylib';
398
+ let shimCode;
399
+ if (isComponent) {
400
+ const componentId = componentIdForFile(filename);
401
+ shimCode = isDev
402
+ ? rustComponentShim(componentId, srcHash, _libExt)
403
+ : rustComponentShimProd(componentId);
404
+ } else if (functions.length > 0) {
405
+ const moduleId = `nativ.${baseName}`;
406
+ const fns = functions.map(f => ({ ...f, args: f.args.map(a => ({ ...a, type: a.tsType || a.type })) }));
407
+ shimCode = isDev
408
+ ? cppFunctionShim(fns, moduleId, srcHash, `nativ_${baseName}`, _libExt)
409
+ : cppFunctionShimProd(fns, moduleId);
410
+ } else {
411
+ shimCode = `// ${path.basename(filename)}: no exports found\nexport {};\n`;
412
+ }
413
+
414
+ return upstreamTransformer.transform({
415
+ filename: filename.replace(/\.rs$/, '.js'),
416
+ src: shimCode,
417
+ options,
418
+ ...rest,
419
+ });
420
+ }
421
+
422
+ // ── Swift files → function or component shim
423
+ if (filename.endsWith('.swift')) {
424
+ const { compileSwiftDylib, extractSwiftExports } = require('./compilers/swift-compiler');
425
+
426
+ const swiftSrc = fs.readFileSync(filename, 'utf8');
427
+ const isSwiftComponent = swiftSrc.includes('@nativ_component') || swiftSrc.includes('nativ::component');
428
+ const moduleId = path.basename(filename, '.swift').toLowerCase();
429
+
430
+ let srcHash = 'prod';
431
+ if (isDev) {
432
+ const origName = isSwiftComponent ? `nativ_${moduleId}` : moduleId;
433
+ srcHash = cachedCompile(src, origName, 'dylib', () => {
434
+ compileSwiftDylib(filename, projectRoot, { target: buildTarget });
435
+ });
436
+ writeManifest(origName, { source: filename, type: isSwiftComponent ? 'swift-component' : 'swift' });
437
+ }
438
+
439
+ if (isSwiftComponent) {
440
+ const componentId = `nativ.${moduleId}`;
441
+ const shimCode = isDev
442
+ ? rustComponentShim(componentId, srcHash)
443
+ : rustComponentShimProd(componentId);
444
+ return upstreamTransformer.transform({
445
+ filename: filename.replace(/\.swift$/, '.js'),
446
+ src: shimCode,
447
+ options,
448
+ ...rest,
449
+ });
450
+ }
451
+
452
+ const exports = extractSwiftExports(filename);
453
+
454
+ if (isDev) {
455
+ try {
456
+ const lines = ['// Auto-generated by React Native Native', ''];
457
+ for (const fn of exports) {
458
+ lines.push(`export declare function ${fn.name}(): ${fn.async ? 'Promise<string>' : 'string'};`);
459
+ }
460
+ lines.push('');
461
+ fs.writeFileSync(dtsPath(filename), lines.join('\n'));
462
+ } catch {}
463
+ }
464
+
465
+ const fns = exports.map(fn => ({ name: fn.name, async: fn.async, args: [] }));
466
+ const shimCode = isDev
467
+ ? cppFunctionShim(fns, moduleId, srcHash, moduleId)
468
+ : cppFunctionShimProd(fns, moduleId);
469
+
470
+ return upstreamTransformer.transform({
471
+ filename: filename.replace(/\.swift$/, '.js'),
472
+ src: shimCode,
473
+ options,
474
+ ...rest,
475
+ });
476
+ }
477
+
478
+ // ── C++/ObjC++ files → function export shim or component
479
+ const isCpp = filename.endsWith('.cpp') || filename.endsWith('.cc');
480
+ const isObjCpp = filename.endsWith('.mm');
481
+
482
+ if (isCpp || isObjCpp) {
483
+ // Resolve include paths once + generate compile_commands.json for clangd
484
+ if (isDev && !_includePaths) {
485
+ _includePaths = getIncludePaths(projectRoot);
486
+ generateCompileCommands(projectRoot, _includePaths);
487
+ }
488
+
489
+ // Check if this is a component (NATIV_COMPONENT / nativ::component)
490
+ const { isCppComponent, extractCppComponentProps } = require('./extractors/cpp-ast-extractor');
491
+ if (isCppComponent(filename)) {
492
+ const baseName = path.basename(filename).replace(/\.(cpp|cc|mm)$/, '').toLowerCase();
493
+ const componentId = `nativ.${baseName}`;
494
+
495
+ let srcHash = 'prod';
496
+ const cppProps = extractCppComponentProps(filename);
497
+ if (isDev) {
498
+ const _cppCompLibExt = isAndroid ? 'so' : 'dylib';
499
+ srcHash = cachedCompile(src, `nativ_${baseName}`, _cppCompLibExt, () => {
500
+ if (isAndroid) compileAndroidCppComponentDylib(filename, _includePaths, projectRoot, baseName, cppProps, { target: buildTarget });
501
+ else compileCppComponentDylib(filename, _includePaths, projectRoot, baseName, cppProps, { target: buildTarget });
502
+ });
503
+ writeManifest(`nativ_${baseName}`, { source: filename, type: 'cpp-component', baseName });
504
+
505
+ try {
506
+ const displayName = path.basename(filename).replace(/\.(cpp|cc|mm)$/, '');
507
+ const propsLines = cppProps.map(p => ` ${p.jsName}?: ${p.tsType};`);
508
+ const dts = [
509
+ `import type { ViewProps } from 'react-native';`,
510
+ ``,
511
+ `interface ${displayName}Props extends ViewProps {`,
512
+ ...propsLines,
513
+ `}`,
514
+ ``,
515
+ `declare const ${displayName}: React.ComponentType<${displayName}Props>;`,
516
+ `export default ${displayName};`,
517
+ `export { ${displayName} };`,
518
+ ``,
519
+ ].join('\n');
520
+ fs.writeFileSync(dtsPath(filename), dts);
521
+ } catch {}
522
+ }
523
+
524
+ const _cppLibExt = isAndroid ? 'so' : 'dylib';
525
+ const shimCode = isDev
526
+ ? rustComponentShim(componentId, srcHash, _cppLibExt)
527
+ : rustComponentShimProd(componentId);
528
+
529
+ return upstreamTransformer.transform({
530
+ filename: filename.replace(/\.(cpp|cc|mm)$/, '.js'),
531
+ src: shimCode,
532
+ options,
533
+ ...rest,
534
+ });
535
+ }
536
+
537
+ // Extract exported functions
538
+ const exports = extractCppExports(filename, _includePaths);
539
+
540
+ if (isDev && exports.length === 0) {
541
+ console.warn(`[nativ] No NATIV_EXPORT functions found in ${path.basename(filename)}`);
542
+ }
543
+
544
+ const moduleId = moduleIdForFile(filename, projectRoot);
545
+
546
+ let srcHash = 'prod';
547
+ if (isDev) {
548
+ const _cppFnLibExt = isAndroid ? 'so' : 'dylib';
549
+ srcHash = cachedCompile(src, moduleId, _cppFnLibExt, () => {
550
+ if (exports.length > 0) {
551
+ if (isAndroid) compileAndroidCppDylib(filename, _includePaths, exports, projectRoot, { target: buildTarget });
552
+ else compileDylib(filename, _includePaths, exports, projectRoot, { target: buildTarget });
553
+ }
554
+ });
555
+ writeManifest(moduleId, { source: filename, type: 'cpp' });
556
+
557
+ try {
558
+ fs.writeFileSync(dtsPath(filename), generateDTS(exports));
559
+ } catch {}
560
+ }
561
+
562
+ const shimCode = isDev
563
+ ? cppFunctionShim(exports, moduleId, srcHash, null, isAndroid ? 'so' : 'dylib')
564
+ : cppFunctionShimProd(exports, moduleId);
565
+
566
+ return upstreamTransformer.transform({
567
+ filename: filename.replace(/\.(cpp|cc|mm)$/, '.js'),
568
+ src: shimCode,
569
+ options,
570
+ ...rest,
571
+ });
572
+ }
573
+
574
+ // ── Kotlin files → function or component shim
575
+ if (filename.endsWith('.kt')) {
576
+ const { functions, isComponent, componentProps } = extractKotlinExports(filename);
577
+ const baseName = path.basename(filename, '.kt');
578
+ const moduleId = baseName.toLowerCase();
579
+
580
+ let srcHash = 'prod';
581
+ if (isDev) {
582
+ srcHash = require('crypto').createHash('md5').update(src).digest('hex').slice(0, 8);
583
+ if (isAndroid) {
584
+ compileKotlinDex(filename, projectRoot);
585
+ }
586
+ writeManifest(moduleId, { source: filename, type: isComponent ? 'kotlin-component' : 'kotlin' });
587
+
588
+ // Generate .d.ts
589
+ try {
590
+ if (isComponent) {
591
+ const propsLines = componentProps
592
+ .filter(p => !p.isCallback)
593
+ .map(p => ` ${p.jsName}?: ${p.tsType};`);
594
+ const cbLines = componentProps
595
+ .filter(p => p.isCallback)
596
+ .map(p => ` ${p.jsName}?: () => void;`);
597
+ const dts = [
598
+ `import type { ViewProps } from 'react-native';`,
599
+ ``,
600
+ `interface ${baseName}Props extends ViewProps {`,
601
+ ...propsLines,
602
+ ...cbLines,
603
+ `}`,
604
+ ``,
605
+ `declare const ${baseName}: React.ComponentType<${baseName}Props>;`,
606
+ `export default ${baseName};`,
607
+ `export { ${baseName} };`,
608
+ ``,
609
+ ].join('\n');
610
+ fs.writeFileSync(dtsPath(filename), dts);
611
+ } else if (functions.length > 0) {
612
+ const lines = ['// Auto-generated by React Native Native', ''];
613
+ for (const fn of functions) {
614
+ const args = fn.args.map(a => `${a.name}: ${a.tsType}`).join(', ');
615
+ const ret = fn.async ? `Promise<${fn.tsType}>` : fn.tsType;
616
+ lines.push(`export declare function ${fn.name}(${args}): ${ret};`);
617
+ }
618
+ lines.push('');
619
+ fs.writeFileSync(dtsPath(filename), lines.join('\n'));
620
+ }
621
+ } catch {}
622
+ }
623
+
624
+ let shimCode;
625
+ if (isComponent) {
626
+ const componentId = `nativ.${moduleId}`;
627
+ shimCode = isDev
628
+ ? rustComponentShim(componentId, srcHash, 'dex')
629
+ : rustComponentShimProd(componentId);
630
+ } else if (functions.length > 0) {
631
+ const fns = functions.map(f => ({ ...f, args: f.args.map(a => ({ ...a, type: a.tsType })) }));
632
+ shimCode = isDev
633
+ ? cppFunctionShim(fns, moduleId, srcHash, moduleId, 'dex')
634
+ : cppFunctionShimProd(fns, moduleId);
635
+ } else {
636
+ shimCode = `// ${baseName}.kt: no exports found\nexport {};\n`;
637
+ }
638
+
639
+ return upstreamTransformer.transform({
640
+ filename: filename.replace(/\.kt$/, '.js'),
641
+ src: shimCode,
642
+ options,
643
+ ...rest,
644
+ });
645
+ }
646
+
647
+ // ── All other files → upstream Babel
648
+ return upstreamTransformer.transform({ filename, src, options, ...rest });
649
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Generates C ABI bridge wrappers for NATIV_EXPORT-annotated functions.
3
+ * Each exported function gets a C-linkage wrapper that the JS runtime calls.
4
+ */
5
+
6
+ function generateCppBridge(exports, moduleId) {
7
+ const safeModuleId = moduleId.replace(/[^a-zA-Z0-9_]/g, '_');
8
+ const lines = [
9
+ `// Auto-generated bridge for ${moduleId}`,
10
+ '#include "Nativ.h"',
11
+ '#include <string>',
12
+ '',
13
+ 'extern "C" {',
14
+ ];
15
+
16
+ for (const fn of exports) {
17
+ if (fn.async) {
18
+ lines.push(`
19
+ void nativ_${safeModuleId}_${fn.name}(
20
+ const char* argsJson,
21
+ void (*resolve)(const char*),
22
+ void (*reject)(const char*, const char*)
23
+ ) {
24
+ try {
25
+ auto result = ${fn.name}(/* TODO: parse args from argsJson */);
26
+ auto json = nativ::toJson(result);
27
+ resolve(json.c_str());
28
+ } catch (const std::exception& e) {
29
+ reject("NATIV_ERROR", e.what());
30
+ } catch (...) {
31
+ reject("NATIV_ERROR", "Unknown error");
32
+ }
33
+ }`);
34
+ } else {
35
+ lines.push(`
36
+ const char* nativ_${safeModuleId}_${fn.name}(const char* argsJson) {
37
+ auto result = ${fn.name}(/* TODO: parse args from argsJson */);
38
+ static thread_local std::string buf;
39
+ buf = nativ::toJson(result);
40
+ return buf.c_str();
41
+ }`);
42
+ }
43
+ }
44
+
45
+ lines.push('');
46
+ lines.push('} // extern "C"');
47
+ return lines.join('\n');
48
+ }
49
+
50
+ module.exports = { generateCppBridge };