@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.
- package/NativFabric.podspec +41 -0
- package/android/build.gradle +128 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/CMakeLists.txt +59 -0
- package/android/src/main/cpp/NativBindingsInstaller.cpp +393 -0
- package/android/src/main/cpp/NativRuntime.cpp +508 -0
- package/android/src/main/java/com/nativfabric/ComposeHost.kt +26 -0
- package/android/src/main/java/com/nativfabric/NativContainerPackage.kt +35 -0
- package/android/src/main/java/com/nativfabric/NativContainerView.kt +51 -0
- package/android/src/main/java/com/nativfabric/NativContainerViewManager.kt +62 -0
- package/android/src/main/java/com/nativfabric/NativRuntime.kt +201 -0
- package/android/src/main/java/com/nativfabric/NativRuntimeModule.kt +37 -0
- package/android/src/main/java/com/nativfabric/compose/ComposeWrappers.kt +45 -0
- package/app.plugin.js +159 -0
- package/expo-module.config.json +6 -0
- package/ios/NativContainerComponentView.mm +137 -0
- package/ios/NativRuntime.h +36 -0
- package/ios/NativRuntime.mm +549 -0
- package/metro/Nativ.h +126 -0
- package/metro/compilers/android-compiler.js +339 -0
- package/metro/compilers/dylib-compiler.js +474 -0
- package/metro/compilers/kotlin-compiler.js +632 -0
- package/metro/compilers/rust-compiler.js +722 -0
- package/metro/compilers/static-compiler.js +1118 -0
- package/metro/compilers/swift-compiler.js +363 -0
- package/metro/extractors/cpp-ast-extractor.js +126 -0
- package/metro/extractors/kotlin-extractor.js +125 -0
- package/metro/extractors/rust-extractor.js +118 -0
- package/metro/index.js +236 -0
- package/metro/transformer.js +649 -0
- package/metro/utils/bridge-generator.js +50 -0
- package/metro/utils/compile-commands.js +104 -0
- package/metro/utils/cpp-daemon.js +344 -0
- package/metro/utils/dts-generator.js +32 -0
- package/metro/utils/include-resolver.js +73 -0
- package/metro/utils/kotlin-daemon.js +394 -0
- package/metro/utils/type-mapper.js +63 -0
- package/package.json +43 -0
- package/react-native.config.js +13 -0
- package/src/NativContainerNativeComponent.ts +9 -0
- package/src/NativeNativRuntime.ts +8 -0
- 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 };
|