@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,118 @@
1
+ /**
2
+ * Extracts #[function(...)] annotated functions from Rust source files.
3
+ * Similar to cpp-ast-extractor.js but for Rust syntax.
4
+ *
5
+ * Supports:
6
+ * #[function] → sync
7
+ * #[function(sync)] → sync
8
+ * #[function(async)] → async (returns Promise in JS)
9
+ *
10
+ * Also detects if the file is a component (has `pub fn render(`)
11
+ */
12
+
13
+ const fs = require('fs');
14
+
15
+ // Map Rust types to TypeScript
16
+ const RUST_TO_TS = {
17
+ 'i8': 'number', 'i16': 'number', 'i32': 'number', 'i64': 'number',
18
+ 'u8': 'number', 'u16': 'number', 'u32': 'number', 'u64': 'number',
19
+ 'f32': 'number', 'f64': 'number',
20
+ 'usize': 'number', 'isize': 'number',
21
+ 'bool': 'boolean',
22
+ 'String': 'string', '&str': 'string',
23
+ '()': 'void',
24
+ };
25
+
26
+ function rustTypeToTS(rustType) {
27
+ const t = rustType.trim();
28
+ if (RUST_TO_TS[t]) return RUST_TO_TS[t];
29
+
30
+ // Result<T, E> → T (we throw on Err)
31
+ const resultMatch = t.match(/^Result\s*<\s*(.+?)\s*,/);
32
+ if (resultMatch) return rustTypeToTS(resultMatch[1]);
33
+
34
+ // Vec<T> → T[]
35
+ const vecMatch = t.match(/^Vec\s*<\s*(.+?)\s*>$/);
36
+ if (vecMatch) return rustTypeToTS(vecMatch[1]) + '[]';
37
+
38
+ // Option<T> → T | null
39
+ const optMatch = t.match(/^Option\s*<\s*(.+?)\s*>$/);
40
+ if (optMatch) return rustTypeToTS(optMatch[1]) + ' | null';
41
+
42
+ return 'unknown';
43
+ }
44
+
45
+ /**
46
+ * Parse a .rs file and return exported functions + whether it's a component.
47
+ */
48
+ function extractRustExports(filepath) {
49
+ let src;
50
+ try {
51
+ src = fs.readFileSync(filepath, 'utf8');
52
+ } catch {
53
+ return { functions: [], isComponent: false };
54
+ }
55
+
56
+ const isComponent = /#\[component\]/.test(src);
57
+
58
+ // Extract component props from struct fields
59
+ const componentProps = [];
60
+ if (isComponent) {
61
+ // Match: pub struct Name { field: Type, ... }
62
+ const structMatch = src.match(/#\[component\]\s*pub\s+struct\s+(\w+)\s*\{([^}]*)\}/s);
63
+ if (structMatch) {
64
+ const fieldsStr = structMatch[2];
65
+ for (const line of fieldsStr.split('\n')) {
66
+ const fieldMatch = line.trim().match(/^(\w+)\s*:\s*(.+?)\s*,?\s*$/);
67
+ if (fieldMatch) {
68
+ const [, name, rustType] = fieldMatch;
69
+ const jsName = snakeToCamel(name);
70
+ const tsType = rustType.trim() === 'Callback'
71
+ ? '(() => void)'
72
+ : rustTypeToTS(rustType.trim());
73
+ componentProps.push({ name, jsName, rustType: rustType.trim(), tsType });
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ const functions = [];
80
+
81
+ // Match: #[function] or #[function(sync)] or #[function(async)] followed by pub fn
82
+ const pattern = /#\[function(?:\s*\(([^)]*)\))?\]\s*pub\s+fn\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([^\{]+))?\s*\{/g;
83
+
84
+ let match;
85
+ while ((match = pattern.exec(src)) !== null) {
86
+ const [, attrs, name, argsStr, retType] = match;
87
+ const isAsync = (attrs || '').includes('async');
88
+
89
+ const args = argsStr
90
+ .split(',')
91
+ .map(a => a.trim())
92
+ .filter(a => a && !a.startsWith('&self'))
93
+ .map(arg => {
94
+ const parts = arg.match(/^(\w+)\s*:\s*(.+)$/);
95
+ if (parts) {
96
+ return { name: parts[1], type: parts[2].trim(), tsType: rustTypeToTS(parts[2].trim()) };
97
+ }
98
+ return { name: '_', type: arg, tsType: 'unknown' };
99
+ });
100
+
101
+ functions.push({
102
+ name,
103
+ async: isAsync,
104
+ args,
105
+ ret: retType ? retType.trim() : '()',
106
+ retTS: retType ? rustTypeToTS(retType.trim()) : 'void',
107
+ });
108
+ }
109
+
110
+ return { functions, isComponent, componentProps };
111
+ }
112
+
113
+ /** Convert snake_case to camelCase (matches the Rust proc macro's to_camel_case) */
114
+ function snakeToCamel(s) {
115
+ return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
116
+ }
117
+
118
+ module.exports = { extractRustExports, rustTypeToTS, snakeToCamel };
package/metro/index.js ADDED
@@ -0,0 +1,236 @@
1
+ /**
2
+ * React Native Native — Metro configuration wrapper.
3
+ *
4
+ * Usage in metro.config.js:
5
+ * const { withReactNativeNative } = require('./metro');
6
+ * module.exports = withReactNativeNative(getDefaultConfig(__dirname));
7
+ */
8
+
9
+ const path = require('path');
10
+ const fs = require('fs');
11
+ const { startDaemon, stopDaemon } = require('./utils/kotlin-daemon');
12
+
13
+ // ── Lazy-loaded compilers (only loaded on first cache miss) ───────────
14
+ let _compilers = null;
15
+ function getCompilers() {
16
+ if (_compilers) return _compilers;
17
+ _compilers = {
18
+ compileDylib: require('./compilers/dylib-compiler').compileDylib,
19
+ compileCppComponentDylib: require('./compilers/dylib-compiler').compileCppComponentDylib,
20
+ compileRustDylib: require('./compilers/rust-compiler').compileRustDylib,
21
+ compileSwiftDylib: require('./compilers/swift-compiler').compileSwiftDylib,
22
+ compileAndroidCppDylib: require('./compilers/android-compiler').compileAndroidCppDylib,
23
+ compileAndroidCppComponentDylib: require('./compilers/android-compiler').compileAndroidCppComponentDylib,
24
+ compileAndroidRustDylib: require('./compilers/android-compiler').compileAndroidRustDylib,
25
+ compileKotlinDex: require('./compilers/kotlin-compiler').compileKotlinDex,
26
+ extractCppExports: require('./extractors/cpp-ast-extractor').extractCppExports,
27
+ extractCppComponentProps: require('./extractors/cpp-ast-extractor').extractCppComponentProps,
28
+ getIncludePaths: require('./utils/include-resolver').getIncludePaths,
29
+ };
30
+ return _compilers;
31
+ }
32
+
33
+ function withReactNativeNative(config) {
34
+ const projectRoot = config.projectRoot || process.cwd();
35
+ fs.mkdirSync(path.join(projectRoot, '.nativ'), { recursive: true });
36
+
37
+
38
+ // ── Detect initial build targets ────────────────────────────────────
39
+ // iOS: booted simulator → 'simulator', otherwise 'device'
40
+ // Updated by middleware when a different target connects.
41
+ let iosTarget = 'device';
42
+ try {
43
+ const { execSync } = require('child_process');
44
+ const simctl = execSync('xcrun simctl list devices booted 2>/dev/null',
45
+ { encoding: 'utf8', timeout: 3000 });
46
+ if (simctl.includes('Booted')) iosTarget = 'simulator';
47
+ } catch {}
48
+ fs.writeFileSync(path.join(projectRoot, '.nativ/ios-target'), iosTarget);
49
+ fs.writeFileSync(path.join(projectRoot, '.nativ/android-target'), 'arm64-v8a');
50
+ console.log(`[nativ] Build targets: iOS=${iosTarget}, Android=arm64-v8a`);
51
+
52
+ // ── Kotlin compiler daemon (dev only) ────────────────────────────────
53
+ const isDev = process.env.NODE_ENV !== 'production';
54
+ if (isDev) {
55
+ startDaemon(projectRoot);
56
+ process.on('exit', stopDaemon);
57
+ process.on('SIGINT', () => { stopDaemon(); process.exit(); });
58
+ process.on('SIGTERM', () => { stopDaemon(); process.exit(); });
59
+ }
60
+
61
+ // ── Source extensions ────────────────────────────────────────────────
62
+ config.resolver.sourceExts.push('rs', 'cpp', 'cc', 'mm', 'swift', 'kt', 'java');
63
+
64
+ // ── Transformer ──────────────────────────────────────────────────────
65
+ config.transformer.babelTransformerPath = path.resolve(__dirname, 'transformer.js');
66
+
67
+ // ── Platform-aware resolution ────────────────────────────────────────
68
+ // .swift/.mm → iOS only, .kt/.java → Android only, .rs/.cpp → both.
69
+ // Bare imports (./Counter) resolve without explicit extension.
70
+ const origResolveRequest = config.resolver.resolveRequest;
71
+ config.resolver.resolveRequest = (context, moduleName, platform) => {
72
+ const defaultResolve = origResolveRequest || context.resolveRequest;
73
+
74
+ if (moduleName.startsWith('.') && !path.extname(moduleName)) {
75
+ const platformExts = {
76
+ ios: ['swift', 'mm'],
77
+ android: ['kt', 'java'],
78
+ };
79
+ const sharedExts = ['rs', 'cpp', 'cc'];
80
+ const tryExts = [...(platformExts[platform] || []), ...sharedExts];
81
+
82
+ for (const ext of tryExts) {
83
+ try {
84
+ return defaultResolve(context, `${moduleName}.${ext}`, platform);
85
+ } catch {}
86
+ }
87
+ }
88
+
89
+ return defaultResolve(context, moduleName, platform);
90
+ };
91
+
92
+ // ── Dylib serving middleware (on-demand compilation) ──────────────────
93
+ // URL: /__nativ_dylib/{target}/{origName}_{hash}.{ext}
94
+ // If the file doesn't exist, compile it using modules.json manifest.
95
+ let _includePaths = null;
96
+ const manifestPath = path.join(projectRoot, '.nativ/modules.json');
97
+
98
+ function compileOnDemand(target, origName, requestedHash, ext) {
99
+ let manifest;
100
+ try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch { return; }
101
+ const entry = manifest[origName];
102
+ if (!entry || !fs.existsSync(entry.source)) return;
103
+
104
+ // Verify source still matches requested hash — skip stale requests
105
+ const currentSource = fs.readFileSync(entry.source, 'utf8');
106
+ const currentHash = require('crypto').createHash('md5').update(currentSource).digest('hex').slice(0, 8);
107
+ if (currentHash !== requestedHash) return; // source changed, request is stale
108
+
109
+ const dylibDir = path.join(projectRoot, '.nativ/dylibs', target);
110
+ const hashedPath = path.join(dylibDir, `${origName}_${currentHash}.${ext}`);
111
+ if (fs.existsSync(hashedPath)) return; // already compiled for this version
112
+
113
+ fs.mkdirSync(dylibDir, { recursive: true });
114
+ const c = getCompilers();
115
+ const isAndroid = ['arm64-v8a', 'armeabi-v7a', 'x86_64', 'x86'].includes(target);
116
+ const opts = { target };
117
+
118
+ console.log(`[nativ] On-demand compile: ${origName} for ${target} (${entry.type})`);
119
+
120
+ // Update last-known target so future transforms compile for this target
121
+ const targetFile = path.join(projectRoot, `.nativ/${isAndroid ? 'android' : 'ios'}-target`);
122
+ try { fs.writeFileSync(targetFile, target); } catch {}
123
+
124
+ try {
125
+ switch (entry.type) {
126
+ case 'rust':
127
+ case 'rust-component':
128
+ if (isAndroid) c.compileAndroidRustDylib(entry.source, projectRoot, opts);
129
+ else c.compileRustDylib(entry.source, projectRoot, opts);
130
+ break;
131
+ case 'swift':
132
+ case 'swift-component':
133
+ c.compileSwiftDylib(entry.source, projectRoot, opts);
134
+ break;
135
+ case 'cpp': {
136
+ if (!_includePaths) _includePaths = c.getIncludePaths(projectRoot);
137
+ const exports = c.extractCppExports(entry.source, _includePaths);
138
+ if (isAndroid) c.compileAndroidCppDylib(entry.source, _includePaths, exports, projectRoot, opts);
139
+ else c.compileDylib(entry.source, _includePaths, exports, projectRoot, opts);
140
+ break;
141
+ }
142
+ case 'cpp-component': {
143
+ if (!_includePaths) _includePaths = c.getIncludePaths(projectRoot);
144
+ const props = c.extractCppComponentProps(entry.source);
145
+ if (isAndroid) c.compileAndroidCppComponentDylib(entry.source, _includePaths, projectRoot, entry.baseName, props, opts);
146
+ else c.compileCppComponentDylib(entry.source, _includePaths, projectRoot, entry.baseName, props, opts);
147
+ break;
148
+ }
149
+ case 'kotlin':
150
+ case 'kotlin-component':
151
+ c.compileKotlinDex(entry.source, projectRoot);
152
+ break;
153
+ default:
154
+ console.error(`[nativ] Unknown module type: ${entry.type}`);
155
+ return;
156
+ }
157
+ } catch (e) {
158
+ console.error(`[nativ] Compile failed for ${origName}: ${e.message}`);
159
+ return;
160
+ }
161
+
162
+ // Copy compiler output to content-hashed filename
163
+ const origPath = path.join(dylibDir, `${origName}.${ext}`);
164
+ if (fs.existsSync(origPath) && !fs.existsSync(hashedPath)) {
165
+ try { fs.copyFileSync(origPath, hashedPath); } catch {}
166
+ }
167
+ }
168
+
169
+ const prevEnhance = config.server?.enhanceMiddleware;
170
+ config.server = {
171
+ ...config.server,
172
+ enhanceMiddleware: (middleware, server) => {
173
+ const base = prevEnhance ? prevEnhance(middleware, server) : middleware;
174
+ return (req, res, next) => {
175
+ if (req.url?.startsWith('/__nativ_dylib/')) {
176
+ const urlPath = req.url.split('?')[0].replace('/__nativ_dylib/', '');
177
+ const filePath = path.join(projectRoot, '.nativ/dylibs', urlPath);
178
+
179
+ // Cache hit — serve immediately
180
+ if (fs.existsSync(filePath)) {
181
+ res.setHeader('Content-Type', 'application/octet-stream');
182
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
183
+ res.setHeader('Pragma', 'no-cache');
184
+ fs.createReadStream(filePath).pipe(res);
185
+ return;
186
+ }
187
+
188
+ // Cache miss — compile on-demand, then re-check
189
+ // Parse: {target}/{origName}_{hash}.{ext}
190
+ const parts = urlPath.split('/');
191
+ if (parts.length === 2) {
192
+ const target = parts[0];
193
+ const m = parts[1].match(/^(.+)_([a-f0-9]{8})\.(dylib|so|dex)$/);
194
+ if (m) {
195
+ const [, origName, hash, ext] = m;
196
+ compileOnDemand(target, origName, hash, ext);
197
+ // Re-check: if source hash matched requested hash, file now exists
198
+ if (fs.existsSync(filePath)) {
199
+ res.setHeader('Content-Type', 'application/octet-stream');
200
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
201
+ res.setHeader('Pragma', 'no-cache');
202
+ fs.createReadStream(filePath).pipe(res);
203
+ return;
204
+ }
205
+ }
206
+ }
207
+
208
+ res.statusCode = 404;
209
+ res.end('not found: ' + urlPath);
210
+ return;
211
+ }
212
+ base(req, res, next);
213
+ };
214
+ },
215
+ };
216
+
217
+ // ── Cache clear hook ─────────────────────────────────────────────────
218
+ // When Metro's cache is cleared (--clear), also wipe compiled native artifacts.
219
+ config.cacheStores = [
220
+ ...(config.cacheStores || []),
221
+ {
222
+ get: async () => null,
223
+ set: async () => {},
224
+ clear: () => {
225
+ const dylibDir = path.join(projectRoot, '.nativ/dylibs');
226
+ try { fs.rmSync(dylibDir, { recursive: true }); } catch {}
227
+ fs.mkdirSync(dylibDir, { recursive: true });
228
+ console.log('[nativ] Cleared compiled native cache (.nativ/dylibs/)');
229
+ },
230
+ },
231
+ ];
232
+
233
+ return config;
234
+ }
235
+
236
+ module.exports = { withReactNativeNative };