@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,632 @@
1
+ /**
2
+ * kotlin-compiler.js — compiles .kt files to .dex for Android hot-reload.
3
+ *
4
+ * Flow: .kt → kotlinc → .class → d8 → .dex → Metro serves → DexClassLoader on device
5
+ *
6
+ * For functions: wraps user code in a class with static methods callable via reflection.
7
+ * For Compose components: wraps in a Composable that renders into ComposeView.
8
+ */
9
+
10
+ const { execSync } = require('child_process');
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+ const { extractKotlinExports } = require('../extractors/kotlin-extractor');
14
+ const { compileSyncViaDaemon, isDaemonReady } = require('../utils/kotlin-daemon');
15
+
16
+ let _kotlincCmd = null; // embeddable: 'java -cp ... K2JVMCompiler'
17
+ let _kotlincComposeCmd = null; // full compiler for Compose: 'java -cp full-compiler.jar:... K2JVMCompiler'
18
+ let _d8Path = null;
19
+ let _androidJar = null;
20
+ let _kotlinStdlib = null;
21
+ let _composePlugin = null; // original (non-instrumented) Compose compiler plugin JAR
22
+ let _composeJarsDir = null; // directory with Compose Android AAR classes.jar
23
+ let _composePretransform = null; // pre-transform supplement JAR (inline bodies)
24
+ let _resolved = false;
25
+
26
+ function resolveOnce(projectRoot) {
27
+ if (_resolved) return;
28
+ _resolved = true;
29
+
30
+ // Try loading cached paths from a previous run (avoids slow `find` commands)
31
+ const cacheFile = projectRoot ? path.join(projectRoot, '.nativ/kotlin-resolve-cache.json') : null;
32
+ if (cacheFile) {
33
+ try {
34
+ const cached = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
35
+ // Verify at least one path still exists
36
+ if (cached._kotlincCmd && cached._androidJar && fs.existsSync(cached._androidJar)) {
37
+ _kotlincCmd = cached._kotlincCmd;
38
+ _kotlincComposeCmd = cached._kotlincComposeCmd;
39
+ _d8Path = cached._d8Path;
40
+ _androidJar = cached._androidJar;
41
+ _kotlinStdlib = cached._kotlinStdlib;
42
+ _composePlugin = cached._composePlugin;
43
+ _composeJarsDir = cached._composeJarsDir;
44
+ _composePretransform = cached._composePretransform;
45
+ if (_kotlincCmd) console.log(`[nativ] kotlinc: resolved (cached)`);
46
+ return;
47
+ }
48
+ } catch {}
49
+ }
50
+
51
+ const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT
52
+ || path.join(process.env.HOME || '', 'Library/Android/sdk');
53
+
54
+ // Find kotlinc — try PATH first, then Gradle cache embeddable JAR
55
+ try {
56
+ _kotlincCmd = execSync('which kotlinc', { encoding: 'utf8' }).trim();
57
+ } catch {}
58
+
59
+ if (!_kotlincCmd) {
60
+ // Fall back to kotlin-compiler-embeddable from Gradle cache
61
+ const gradleCache = path.join(process.env.HOME || '', '.gradle/caches/modules-2/files-2.1');
62
+ try {
63
+ // The embeddable compiler needs its deps on the JVM classpath
64
+ const jvmCp = [];
65
+ const findJar = (group, artifact) => {
66
+ try {
67
+ return execSync(
68
+ `find "${gradleCache}/${group}/${artifact}" -name "*.jar" -not -name "*sources*" -not -name "*javadoc*" 2>/dev/null | sort -V | tail -1`,
69
+ { encoding: 'utf8' }
70
+ ).trim();
71
+ } catch { return ''; }
72
+ };
73
+
74
+ const deps = [
75
+ ['org.jetbrains.kotlin', 'kotlin-compiler-embeddable'],
76
+ ['org.jetbrains.kotlin', 'kotlin-stdlib'],
77
+ ['org.jetbrains.kotlin', 'kotlin-script-runtime'],
78
+ ['org.jetbrains.kotlin', 'kotlin-reflect'],
79
+ ['org.jetbrains.kotlin', 'kotlin-daemon-embeddable'],
80
+ ['org.jetbrains.kotlinx', 'kotlinx-coroutines-core-jvm'],
81
+ ['org.jetbrains.intellij.deps', 'trove4j'],
82
+ ['org.jetbrains', 'annotations'],
83
+ ];
84
+
85
+ for (const [group, artifact] of deps) {
86
+ const jarPath = findJar(group, artifact);
87
+ if (jarPath && fs.existsSync(jarPath)) jvmCp.push(jarPath);
88
+ }
89
+
90
+ if (jvmCp.length >= 3) { // compiler + stdlib + coroutines minimum
91
+ _kotlincCmd = `java -cp "${jvmCp.join(':')}" org.jetbrains.kotlin.cli.jvm.K2JVMCompiler`;
92
+ }
93
+ } catch {}
94
+ }
95
+
96
+ // Find kotlin-stdlib jar matching the compiler version
97
+ // (version mismatch causes metadata incompatibility errors)
98
+ try {
99
+ const gradleCache = path.join(process.env.HOME || '', '.gradle/caches/modules-2/files-2.1');
100
+ // Extract compiler version from the kotlinc command or JARs
101
+ let kotlinVersion = null;
102
+ if (_kotlincCmd && _kotlincCmd.includes('kotlin-compiler-embeddable')) {
103
+ const verMatch = _kotlincCmd.match(/kotlin-compiler-embeddable-(\d+\.\d+\.\d+)/);
104
+ if (verMatch) kotlinVersion = verMatch[1];
105
+ }
106
+
107
+ const versionFilter = kotlinVersion
108
+ ? `-name "kotlin-stdlib-${kotlinVersion}.jar"`
109
+ : `-name "kotlin-stdlib-*.jar" -not -name "*sources*"`;
110
+
111
+ const stdlibPath = execSync(
112
+ `find "${gradleCache}/org.jetbrains.kotlin/kotlin-stdlib" ${versionFilter} 2>/dev/null | sort -V | tail -1`,
113
+ { encoding: 'utf8' }
114
+ ).trim();
115
+ if (stdlibPath && fs.existsSync(stdlibPath)) {
116
+ _kotlinStdlib = stdlibPath;
117
+ }
118
+ } catch {}
119
+
120
+ // Find d8 from build-tools, fall back to local cache
121
+ const btDir = path.join(androidHome, 'build-tools');
122
+ if (fs.existsSync(btDir)) {
123
+ const versions = fs.readdirSync(btDir).sort();
124
+ if (versions.length > 0) {
125
+ const d8 = path.join(btDir, versions[versions.length - 1], 'd8');
126
+ if (fs.existsSync(d8)) _d8Path = d8;
127
+ }
128
+ }
129
+ if (!_d8Path && projectRoot) {
130
+ const localD8 = path.join(projectRoot, '.nativ/kotlin-cache/d8.jar');
131
+ if (fs.existsSync(localD8)) _d8Path = `java -jar "${localD8}"`;
132
+ }
133
+
134
+ // Find android.jar for compilation classpath, fall back to local cache
135
+ const platformsDir = path.join(androidHome, 'platforms');
136
+ if (fs.existsSync(platformsDir)) {
137
+ const platforms = fs.readdirSync(platformsDir).sort();
138
+ if (platforms.length > 0) {
139
+ const jar = path.join(platformsDir, platforms[platforms.length - 1], 'android.jar');
140
+ if (fs.existsSync(jar)) _androidJar = jar;
141
+ }
142
+ }
143
+ if (!_androidJar && projectRoot) {
144
+ const localJar = path.join(projectRoot, '.nativ/kotlin-cache/android.jar');
145
+ if (fs.existsSync(localJar)) _androidJar = localJar;
146
+ }
147
+
148
+ // Find Compose compiler plugin (original, non-instrumented) + full compiler
149
+ try {
150
+ const gradleCache = path.join(process.env.HOME || '', '.gradle/caches/modules-2/files-2.1');
151
+ // The original plugin JAR (needs the non-embeddable compiler)
152
+ const plugin = execSync(
153
+ `find "${gradleCache}/org.jetbrains.kotlin/kotlin-compose-compiler-plugin" -name "*.jar" -not -name "*sources*" 2>/dev/null | sort -V | tail -1`,
154
+ { encoding: 'utf8' }
155
+ ).trim();
156
+ if (plugin && fs.existsSync(plugin)) _composePlugin = plugin;
157
+
158
+ // Build the full (non-embeddable) compiler command for Compose
159
+ // The full compiler has un-shaded com.intellij.* classes that the Compose plugin needs
160
+ if (_composePlugin && projectRoot) {
161
+ // Read Kotlin version from config, fall back to scanning for any kotlin-compiler-*.jar
162
+ let _ktVersion = null;
163
+ try {
164
+ const cfg = JSON.parse(fs.readFileSync(path.join(projectRoot, '.nativ/nativ.config.json'), 'utf8'));
165
+ _ktVersion = cfg.kotlin?.version;
166
+ } catch {}
167
+ const fullCompiler = _ktVersion
168
+ ? path.join(projectRoot, `.nativ/compose-pretransform/kotlin-compiler-${_ktVersion}.jar`)
169
+ : (() => {
170
+ try {
171
+ const files = fs.readdirSync(path.join(projectRoot, '.nativ/compose-pretransform'));
172
+ const match = files.find(f => f.startsWith('kotlin-compiler-') && f.endsWith('.jar'));
173
+ return match ? path.join(projectRoot, '.nativ/compose-pretransform', match) : '';
174
+ } catch { return ''; }
175
+ })();
176
+ if (fs.existsSync(fullCompiler)) {
177
+ const findJar = (group, artifact) => {
178
+ try {
179
+ return execSync(
180
+ `find "${gradleCache}/${group}/${artifact}" -name "*.jar" -not -name "*sources*" -not -name "*javadoc*" 2>/dev/null | sort -V | tail -1`,
181
+ { encoding: 'utf8' }
182
+ ).trim();
183
+ } catch { return ''; }
184
+ };
185
+
186
+ const jvmDeps = [
187
+ fullCompiler,
188
+ findJar('org.jetbrains.kotlin', 'kotlin-stdlib'),
189
+ findJar('org.jetbrains.kotlin', 'kotlin-script-runtime'),
190
+ findJar('org.jetbrains.kotlinx', 'kotlinx-coroutines-core-jvm'),
191
+ findJar('org.jetbrains.intellij.deps', 'trove4j'),
192
+ findJar('org.jetbrains', 'annotations'),
193
+ ].filter(Boolean);
194
+
195
+ if (jvmDeps.length >= 4) {
196
+ _kotlincComposeCmd = `java -cp "${jvmDeps.join(':')}" org.jetbrains.kotlin.cli.jvm.K2JVMCompiler`;
197
+ }
198
+ }
199
+ }
200
+ } catch {}
201
+
202
+ // Set up Compose JARs directory (Android AAR classes.jar for runtime resolution)
203
+ if (projectRoot) {
204
+ _composeJarsDir = path.join(projectRoot, '.nativ/compose-jars');
205
+ if (!fs.existsSync(_composeJarsDir) || fs.readdirSync(_composeJarsDir).length === 0) {
206
+ _composeJarsDir = null;
207
+ }
208
+ // Pre-transform supplement JAR (inline bodies for remember, Box, Column, Row)
209
+ const ptJar = path.join(projectRoot, '.nativ/compose-pretransform/compose-pretransform-1.7.0.jar');
210
+ if (fs.existsSync(ptJar)) _composePretransform = ptJar;
211
+ }
212
+
213
+ if (_kotlincCmd) console.log(`[nativ] kotlinc: ${_kotlincCmd.split(' ')[0] === 'java' ? 'embeddable JAR' : _kotlincCmd}`);
214
+ if (_d8Path) console.log(`[nativ] d8: ${_d8Path}`);
215
+ if (_androidJar) console.log(`[nativ] android.jar: ${path.basename(path.dirname(_androidJar))}`);
216
+ if (_kotlincComposeCmd) console.log(`[nativ] compose: full compiler + plugin + pretransform`);
217
+ else if (_composePlugin) console.log(`[nativ] compose: plugin found but missing full compiler`);
218
+
219
+ // Cache resolved paths for fast startup in Metro workers
220
+ if (cacheFile) {
221
+ try {
222
+ fs.writeFileSync(cacheFile, JSON.stringify({
223
+ _kotlincCmd, _kotlincComposeCmd, _d8Path, _androidJar, _kotlinStdlib,
224
+ _composePlugin, _composeJarsDir, _composePretransform,
225
+ }));
226
+ } catch {}
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Compile a Kotlin function file to .dex for hot-reload.
232
+ * Returns path to the .dex file, or null on failure.
233
+ */
234
+ function compileKotlinDex(filepath, projectRoot) {
235
+ resolveOnce(projectRoot);
236
+ if (!_kotlincCmd || !_d8Path || !_androidJar) {
237
+ console.warn('[nativ] Kotlin toolchain incomplete — skipping compilation');
238
+ return null;
239
+ }
240
+
241
+ const name = path.basename(filepath, '.kt');
242
+ const moduleId = name.toLowerCase();
243
+ const outputDir = path.join(projectRoot, '.nativ/dylibs');
244
+ const buildDir = path.join(projectRoot, '.nativ/build', `kt_${moduleId}`);
245
+ fs.mkdirSync(buildDir, { recursive: true });
246
+ fs.mkdirSync(outputDir, { recursive: true });
247
+
248
+ const { functions, isComponent, componentProps } = extractKotlinExports(filepath);
249
+
250
+ if (!isComponent && functions.length === 0) {
251
+ console.warn(`[nativ] No @nativ_export/@nativ_component in ${name}.kt`);
252
+ return null;
253
+ }
254
+
255
+ // Content-addressed cache: each source hash gets its own .dex
256
+ // Reverts (A→B→A) are instant — the .dex for A is already on disk.
257
+ const srcHash = require('crypto').createHash('md5').update(fs.readFileSync(filepath)).digest('hex').slice(0, 8);
258
+ const className = `NativModule_${moduleId}`;
259
+ const dexBase = isComponent ? `nativ_${moduleId}` : moduleId;
260
+ const dexName = `${dexBase}_${srcHash}.dex`;
261
+ const dexPath = path.join(outputDir, dexName);
262
+
263
+ if (fs.existsSync(dexPath)) {
264
+ console.log(`[nativ] ${name}.kt cache hit (${srcHash})`);
265
+ return dexPath;
266
+ }
267
+
268
+ if (isComponent) {
269
+ return compileKotlinComponent(filepath, projectRoot, name, moduleId,
270
+ componentProps, buildDir, dexPath, className);
271
+ }
272
+
273
+ // Generate wrapper class with static methods
274
+ const wrapperSrc = generateFunctionWrapper(filepath, functions, className, moduleId);
275
+ const wrapperPath = path.join(buildDir, `${className}.kt`);
276
+ fs.writeFileSync(wrapperPath, wrapperSrc);
277
+
278
+ return compileAndDex(wrapperPath, buildDir, dexPath, moduleId);
279
+ }
280
+
281
+ function generateFunctionWrapper(filepath, functions, className, moduleId) {
282
+ const userSrc = fs.readFileSync(filepath, 'utf8');
283
+
284
+ // Strip @nativ_export comments and existing package declaration
285
+ const cleanSrc = userSrc
286
+ .replace(/\/\/\s*@nativ_export\s*\([^)]*\)\s*\n/g, '')
287
+ .replace(/^package\s+[^\n]+\n/m, '');
288
+
289
+ const lines = [
290
+ `// Auto-generated wrapper for ${moduleId}.kt`,
291
+ `package com.nativfabric.generated`,
292
+ '',
293
+ ];
294
+
295
+ // Include the user's functions
296
+ lines.push(cleanSrc);
297
+ lines.push('');
298
+
299
+ // Minimal JSON array parser (no external deps — avoids android.jar issues)
300
+ lines.push(`private fun _parseArgs(json: String): List<String> {`);
301
+ lines.push(` val s = json.trim().removePrefix("[").removeSuffix("]")`);
302
+ lines.push(` if (s.isBlank()) return emptyList()`);
303
+ lines.push(` val result = mutableListOf<String>()`);
304
+ lines.push(` var i = 0; var inStr = false; var esc = false; val buf = StringBuilder()`);
305
+ lines.push(` while (i < s.length) {`);
306
+ lines.push(` val c = s[i]`);
307
+ lines.push(` when {`);
308
+ lines.push(` esc -> { buf.append(c); esc = false }`);
309
+ lines.push(` c == '\\\\' -> esc = true`);
310
+ lines.push(` c == '"' -> inStr = !inStr`);
311
+ lines.push(` c == ',' && !inStr -> { result.add(buf.toString().trim()); buf.clear() }`);
312
+ lines.push(` else -> buf.append(c)`);
313
+ lines.push(` }; i++`);
314
+ lines.push(` }`);
315
+ lines.push(` if (buf.isNotEmpty()) result.add(buf.toString().trim())`);
316
+ lines.push(` return result`);
317
+ lines.push(`}`);
318
+ lines.push('');
319
+
320
+ // Generate a class with a dispatch method
321
+ lines.push(`object ${className} {`);
322
+ lines.push(' @JvmStatic');
323
+ lines.push(' fun dispatch(fnName: String, argsJson: String): String {');
324
+ lines.push(' val args = _parseArgs(argsJson)');
325
+ lines.push(' return when (fnName) {');
326
+
327
+ for (const fn of functions) {
328
+ const argExprs = fn.args.map((a, i) => {
329
+ switch (a.type) {
330
+ case 'Int': return `args[${i}].toInt()`;
331
+ case 'Long': return `args[${i}].toLong()`;
332
+ case 'Float': return `args[${i}].toFloat()`;
333
+ case 'Double': return `args[${i}].toDouble()`;
334
+ case 'Boolean': return `args[${i}].toBoolean()`;
335
+ case 'String': return `args[${i}]`;
336
+ default: return `args[${i}]`;
337
+ }
338
+ });
339
+
340
+ const call = `${fn.name}(${argExprs.join(', ')})`;
341
+
342
+ if (fn.ret === 'String') {
343
+ lines.push(` "${fn.name}" -> "\\"" + ${call} + "\\""`);
344
+ } else if (fn.ret === 'Unit') {
345
+ lines.push(` "${fn.name}" -> { ${call}; "null" }`);
346
+ } else {
347
+ lines.push(` "${fn.name}" -> ${call}.toString()`);
348
+ }
349
+ }
350
+
351
+ lines.push(' else -> "null"');
352
+ lines.push(' }');
353
+ lines.push(' }');
354
+ lines.push('}');
355
+
356
+ return lines.join('\n');
357
+ }
358
+
359
+ function compileKotlinComponent(filepath, projectRoot, name, moduleId,
360
+ componentProps, buildDir, dexPath, className) {
361
+ const userSrc = fs.readFileSync(filepath, 'utf8');
362
+ const isCompose = userSrc.includes('@Composable');
363
+
364
+ // Strip annotations, package, imports — wrapper provides its own
365
+ const cleanSrc = userSrc
366
+ .replace(/\/\/\s*@nativ_component\s*\n/g, '')
367
+ .replace(/^package\s+[^\n]+\n/m, '')
368
+ .replace(/^import\s+[^\n]+\n/gm, '');
369
+
370
+ let userImports = [...userSrc.matchAll(/^(import\s+[^\n]+)\n/gm)]
371
+ .map(m => m[1]);
372
+
373
+ if (isCompose) {
374
+ // Rewrite imports for inline layout functions to use our non-inline wrappers
375
+ const wrapperRewrites = {
376
+ 'androidx.compose.foundation.layout.Box': 'com.nativfabric.compose.Box',
377
+ 'androidx.compose.foundation.layout.Column': 'com.nativfabric.compose.Column',
378
+ 'androidx.compose.foundation.layout.Row': 'com.nativfabric.compose.Row',
379
+ 'androidx.compose.foundation.layout.Spacer': 'com.nativfabric.compose.Spacer',
380
+ };
381
+ userImports = userImports.map(imp => {
382
+ for (const [from, to] of Object.entries(wrapperRewrites)) {
383
+ if (imp.includes(from)) return imp.replace(from, to);
384
+ }
385
+ // Rewrite wildcard import for foundation.layout
386
+ if (imp.includes('androidx.compose.foundation.layout.*')) {
387
+ return imp + '\nimport com.nativfabric.compose.*';
388
+ }
389
+ return imp;
390
+ });
391
+ // Compose component — needs ComposeView wrapper (requires Compose compiler plugin)
392
+ const lines = [
393
+ `// Auto-generated Compose component wrapper for ${moduleId}.kt`,
394
+ `package com.nativfabric.generated`,
395
+ '',
396
+ 'import android.view.ViewGroup',
397
+ 'import android.widget.FrameLayout',
398
+ 'import androidx.compose.runtime.*',
399
+ 'import androidx.compose.ui.platform.ComposeView',
400
+ ...userImports,
401
+ '',
402
+ cleanSrc,
403
+ '',
404
+ `object ${className} {`,
405
+ ' @JvmStatic',
406
+ ' fun render(parent: ViewGroup, props: Map<String, Any?>) {',
407
+ ' // ComposeView + setContent compiled in same pass as user code',
408
+ ' // so the Compose plugin transforms everything consistently',
409
+ ' val composeView = ComposeView(parent.context)',
410
+ ' composeView.setContent {',
411
+ ];
412
+
413
+ const compFnMatch = cleanSrc.match(/@Composable\s+fun\s+(\w+)\s*\(([^)]*)\)/);
414
+ const compFnName = compFnMatch ? compFnMatch[1] : name;
415
+ const compParams = compFnMatch && compFnMatch[2] ? compFnMatch[2].trim() : '';
416
+
417
+ if (compParams) {
418
+ // Parse params and generate prop extraction
419
+ const args = compParams.split(',').map(p => p.trim()).filter(Boolean);
420
+ const argExprs = args.map(p => {
421
+ const m = p.match(/(\w+)\s*:\s*(.+)/);
422
+ if (!m) return null;
423
+ const [, pName, pType] = m;
424
+ const t = pType.trim();
425
+ if (t === 'String') return ` ${pName} = props["${pName}"] as? String ?: ""`;
426
+ if (t === 'Int') return ` ${pName} = (props["${pName}"] as? Number)?.toInt() ?: 0`;
427
+ if (t === 'Float' || t === 'Double') return ` ${pName} = (props["${pName}"] as? Number)?.toDouble() ?: 0.0`;
428
+ if (t === 'Boolean') return ` ${pName} = props["${pName}"] as? Boolean ?: false`;
429
+ if (t.includes('->')) return ` ${pName} = {}`;
430
+ return ` ${pName} = props["${pName}"]`;
431
+ }).filter(Boolean);
432
+
433
+ lines.push(` ${compFnName}(`);
434
+ lines.push(argExprs.join(',\n'));
435
+ lines.push(' )');
436
+ } else {
437
+ lines.push(` ${compFnName}()`);
438
+ }
439
+ lines.push(' }');
440
+ lines.push(' parent.addView(composeView, FrameLayout.LayoutParams(');
441
+ lines.push(' FrameLayout.LayoutParams.MATCH_PARENT,');
442
+ lines.push(' FrameLayout.LayoutParams.MATCH_PARENT))');
443
+ lines.push(' }');
444
+ lines.push('}');
445
+
446
+ const wrapperPath = path.join(buildDir, `${className}.kt`);
447
+ fs.writeFileSync(wrapperPath, lines.join('\n'));
448
+ return compileAndDex(wrapperPath, buildDir, dexPath, moduleId, true, projectRoot);
449
+ }
450
+
451
+ // Plain View component — user function has signature: fun Name(parent: ViewGroup, props: Map<...>)
452
+ // Just wrap it in an object with a static render() that delegates to the user function.
453
+ const fnMatch = cleanSrc.match(/fun\s+(\w+)\s*\(\s*parent\s*:/);
454
+ const fnName = fnMatch ? fnMatch[1] : name;
455
+
456
+ const lines = [
457
+ `// Auto-generated View component wrapper for ${moduleId}.kt`,
458
+ `package com.nativfabric.generated`,
459
+ '',
460
+ ...userImports,
461
+ '',
462
+ cleanSrc,
463
+ '',
464
+ `object ${className} {`,
465
+ ' @JvmStatic',
466
+ ' fun render(parent: android.view.ViewGroup, props: Map<String, Any?>) {',
467
+ ` ${fnName}(parent, props)`,
468
+ ' }',
469
+ '}',
470
+ ];
471
+
472
+ const wrapperPath = path.join(buildDir, `${className}.kt`);
473
+ fs.writeFileSync(wrapperPath, lines.join('\n'));
474
+ return compileAndDex(wrapperPath, buildDir, dexPath, moduleId);
475
+ }
476
+
477
+ function compileAndDex(ktPath, buildDir, dexPath, moduleId, isCompose, projectRoot) {
478
+ const classDir = path.join(buildDir, 'classes');
479
+ // Clean previous classes to avoid stale files
480
+ try { fs.rmSync(classDir, { recursive: true }); } catch {}
481
+ fs.mkdirSync(classDir, { recursive: true });
482
+
483
+ // Step 1: kotlinc → .class files
484
+ const cp = [_androidJar];
485
+ if (_kotlinStdlib) cp.push(_kotlinStdlib);
486
+
487
+ // Add Compose JARs for Compose components
488
+ if (isCompose) {
489
+ // Pre-transform JAR for remember inline body
490
+ if (_composePretransform) cp.unshift(_composePretransform);
491
+ // Non-inline wrappers for Box/Column/Row/Spacer (compiled with Compose plugin)
492
+ const wrappersJar = _composePretransform
493
+ ? path.join(path.dirname(_composePretransform), 'compose-wrappers.jar')
494
+ : null;
495
+ if (wrappersJar && fs.existsSync(wrappersJar)) cp.unshift(wrappersJar);
496
+
497
+ // Published Android AAR classes.jar for all other types
498
+ if (_composeJarsDir) {
499
+ try {
500
+ const jars = fs.readdirSync(_composeJarsDir)
501
+ .filter(f => f.endsWith('.jar'))
502
+ .map(f => path.join(_composeJarsDir, f));
503
+ cp.push(...jars);
504
+ } catch {}
505
+ }
506
+
507
+ // ComposeHost JAR (built by setup-compose, no Gradle build needed)
508
+ if (projectRoot) {
509
+ const hostJar = path.join(projectRoot, '.nativ/compose-pretransform/compose-host.jar');
510
+ if (fs.existsSync(hostJar)) cp.push(hostJar);
511
+ }
512
+ }
513
+
514
+ // Use full (non-embeddable) compiler for Compose, embeddable for everything else
515
+ const compilerCmd = (isCompose && _kotlincComposeCmd) ? _kotlincComposeCmd : _kotlincCmd;
516
+
517
+ const kotlincCmd = [
518
+ compilerCmd,
519
+ ktPath,
520
+ '-d', classDir,
521
+ '-classpath', cp.join(':'),
522
+ '-no-reflect',
523
+ '-jvm-target', '17',
524
+ ];
525
+
526
+ // Add Compose compiler plugin via -Xplugin (only works with full compiler)
527
+ if (isCompose && _composePlugin && _kotlincComposeCmd) {
528
+ kotlincCmd.push(`-Xplugin=${_composePlugin}`);
529
+ }
530
+
531
+ // Step 1: kotlinc → .class
532
+ const _t0 = Date.now();
533
+ let compiled = false;
534
+ let via = 'execSync';
535
+
536
+ if (!isCompose && isDaemonReady()) {
537
+ const result = compileSyncViaDaemon({
538
+ sourceFile: ktPath,
539
+ outputDir: classDir,
540
+ classpath: cp.join(':'),
541
+ plugin: '',
542
+ dexOutput: dexPath,
543
+ androidJar: _androidJar || '',
544
+ });
545
+ if (result && result.success) {
546
+ compiled = true;
547
+ via = `daemon(kotlinc=${result.kotlincMs || result.ms || '?'}ms, d8=${result.d8Ms || '?'}ms)`;
548
+ if (result.d8Ms !== undefined && result.d8Ms > 0) {
549
+ // Daemon did kotlinc+d8 — skip the separate d8 step below
550
+ const size = fs.existsSync(dexPath) ? fs.statSync(dexPath).size : 0;
551
+ console.log(`[nativ] ${moduleId}.kt: ${via}, total ${Date.now() - _t0}ms → ${(size / 1024).toFixed(1)}KB`);
552
+ return dexPath;
553
+ }
554
+ } else if (result) {
555
+ console.error(`[nativ] Daemon compile failed: ${result.error?.slice(0, 500)}`);
556
+ }
557
+ }
558
+
559
+ if (!compiled) {
560
+ try {
561
+ execSync(kotlincCmd.join(' '), { stdio: 'pipe', encoding: 'utf8' });
562
+ } catch (err) {
563
+ console.error(`[nativ] Kotlin compile failed: ${moduleId}.kt`);
564
+ console.error((err.stderr || '').slice(0, 2000));
565
+ return null;
566
+ }
567
+ }
568
+
569
+ const _t1 = Date.now();
570
+ console.log(`[nativ] ${moduleId}.kt: kotlinc ${via} ${_t1 - _t0}ms`);
571
+
572
+ // Step 2: d8 → .dex
573
+ // Find all .class files
574
+ const classFiles = [];
575
+ function findClasses(dir) {
576
+ for (const f of fs.readdirSync(dir)) {
577
+ const full = path.join(dir, f);
578
+ if (fs.statSync(full).isDirectory()) findClasses(full);
579
+ else if (f.endsWith('.class')) classFiles.push(full);
580
+ }
581
+ }
582
+ findClasses(classDir);
583
+
584
+ if (classFiles.length === 0) {
585
+ console.error(`[nativ] No .class files produced for ${moduleId}.kt`);
586
+ return null;
587
+ }
588
+
589
+ const d8Cmd = [
590
+ _d8Path,
591
+ '--output', path.dirname(dexPath),
592
+ '--lib', _androidJar,
593
+ '--min-api', '24',
594
+ '--no-desugaring',
595
+ ];
596
+
597
+ // Add classpath for d8 to resolve references to Compose/stdlib classes
598
+ if (_kotlinStdlib) d8Cmd.push('--classpath', _kotlinStdlib);
599
+ if (isCompose && _composeJarsDir) {
600
+ try {
601
+ for (const f of fs.readdirSync(_composeJarsDir)) {
602
+ if (f.endsWith('.jar')) d8Cmd.push('--classpath', path.join(_composeJarsDir, f));
603
+ }
604
+ } catch {}
605
+ }
606
+
607
+ // Write class files to an argfile to avoid shell $ expansion in filenames
608
+ const d8ArgFile = path.join(buildDir, 'd8-args.txt');
609
+ fs.writeFileSync(d8ArgFile, classFiles.join('\n'));
610
+
611
+ const _t2 = Date.now();
612
+ try {
613
+ execSync(d8Cmd.join(' ') + ` @${d8ArgFile}`, { stdio: 'pipe', encoding: 'utf8' });
614
+ // d8 outputs classes.dex — rename to our target name
615
+ const d8Output = path.join(path.dirname(dexPath), 'classes.dex');
616
+ if (fs.existsSync(d8Output)) {
617
+ fs.renameSync(d8Output, dexPath);
618
+ }
619
+ } catch (err) {
620
+ console.error(`[nativ] d8 failed: ${moduleId}`);
621
+ console.error((err.stderr || '').slice(0, 1000));
622
+ return null;
623
+ }
624
+
625
+ const _t3 = Date.now();
626
+ const size = fs.statSync(dexPath).size;
627
+ console.log(`[nativ] ${moduleId}.kt: d8 ${_t3 - _t2}ms → ${(size / 1024).toFixed(1)}KB (total ${_t3 - _t0}ms)`);
628
+
629
+ return dexPath;
630
+ }
631
+
632
+ module.exports = { compileKotlinDex, extractKotlinExports };