@react-native-native/cli 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.
@@ -0,0 +1,467 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * setup-kotlin.js — Sets up the Kotlin/Compose dev hot-reload toolchain.
5
+ *
6
+ * Downloads the non-embeddable Kotlin compiler from Maven, locates the
7
+ * Compose compiler plugin and AAR JARs from the Gradle cache, and builds
8
+ * the pre-transform stubs needed for standalone kotlinc compilation.
9
+ *
10
+ * Run: npx @react-native-native/cli setup-kotlin
11
+ * Or: node packages/cli/scripts/setup-kotlin.js
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { execSync } = require('child_process');
17
+ const https = require('https');
18
+
19
+ // ─── Config ────────────────────────────────────────────────────────────
20
+
21
+ const projectRoot = process.cwd();
22
+
23
+ // Read Kotlin version from nativ.config.json (written by setup-kotlin)
24
+ function readKotlinVersion() {
25
+ try {
26
+ const config = JSON.parse(fs.readFileSync(path.join(projectRoot, '.nativ/nativ.config.json'), 'utf8'));
27
+ return config.kotlin?.version;
28
+ } catch {}
29
+ return null;
30
+ }
31
+
32
+ const KOTLIN_VERSION = readKotlinVersion() || '2.0.21';
33
+ const COMPOSE_VERSION = '1.7.0';
34
+ const COMPOSE_MATERIAL3_VERSION = '1.3.0';
35
+ const outDir = path.join(projectRoot, '.nativ/compose-pretransform');
36
+ const composeJarsDir = path.join(projectRoot, '.nativ/compose-jars');
37
+ const gradleCache = path.join(process.env.HOME || '', '.gradle/caches/modules-2/files-2.1');
38
+
39
+ // ─── Helpers ───────────────────────────────────────────────────────────
40
+
41
+ function download(url, dest) {
42
+ return new Promise((resolve, reject) => {
43
+ console.log(` Downloading ${path.basename(dest)}...`);
44
+ const file = fs.createWriteStream(dest);
45
+ const request = (url.startsWith('https') ? https : require('http')).get(url, (response) => {
46
+ if (response.statusCode === 301 || response.statusCode === 302) {
47
+ file.close();
48
+ fs.unlinkSync(dest);
49
+ return download(response.headers.location, dest).then(resolve, reject);
50
+ }
51
+ response.pipe(file);
52
+ file.on('finish', () => { file.close(); resolve(); });
53
+ });
54
+ request.on('error', (err) => { fs.unlinkSync(dest); reject(err); });
55
+ });
56
+ }
57
+
58
+ function findInGradleCache(group, artifact, nameFilter, version) {
59
+ const dir = path.join(gradleCache, group, artifact);
60
+ if (!fs.existsSync(dir)) return null;
61
+ try {
62
+ // If version specified, look in that version's directory first
63
+ if (version) {
64
+ const versionDir = path.join(dir, version);
65
+ if (fs.existsSync(versionDir)) {
66
+ const result = execSync(
67
+ `find "${versionDir}" -name "${nameFilter || '*.jar'}" -not -name "*sources*" -not -name "*javadoc*" 2>/dev/null | head -1`,
68
+ { encoding: 'utf8' }
69
+ ).trim();
70
+ if (result && fs.existsSync(result)) return result;
71
+ }
72
+ }
73
+ // Fall back to latest version
74
+ const result = execSync(
75
+ `find "${dir}" -name "${nameFilter || '*.jar'}" -not -name "*sources*" -not -name "*javadoc*" 2>/dev/null | sort -V | tail -1`,
76
+ { encoding: 'utf8' }
77
+ ).trim();
78
+ return result && fs.existsSync(result) ? result : null;
79
+ } catch { return null; }
80
+ }
81
+
82
+ function findAarClassesJar(group, artifact) {
83
+ const dir = path.join(gradleCache, group, artifact);
84
+ if (!fs.existsSync(dir)) return null;
85
+ try {
86
+ // Find the AAR, extract classes.jar from it
87
+ const aar = execSync(
88
+ `find "${dir}" -name "*.aar" 2>/dev/null | sort -V | tail -1`,
89
+ { encoding: 'utf8' }
90
+ ).trim();
91
+ if (!aar) return null;
92
+
93
+ // Check if already extracted
94
+ const name = `${artifact.replace(/\//g, '-')}-${path.basename(path.dirname(path.dirname(aar)))}.jar`;
95
+ const dest = path.join(composeJarsDir, name);
96
+ if (fs.existsSync(dest)) return dest;
97
+
98
+ // Extract classes.jar from AAR (it's a zip)
99
+ const tmpDir = path.join(outDir, '_tmp_aar');
100
+ fs.mkdirSync(tmpDir, { recursive: true });
101
+ execSync(`unzip -o -q "${aar}" classes.jar -d "${tmpDir}" 2>/dev/null`, { stdio: 'pipe' });
102
+ const classesJar = path.join(tmpDir, 'classes.jar');
103
+ if (fs.existsSync(classesJar)) {
104
+ fs.copyFileSync(classesJar, dest);
105
+ fs.rmSync(tmpDir, { recursive: true });
106
+ return dest;
107
+ }
108
+ fs.rmSync(tmpDir, { recursive: true });
109
+ return null;
110
+ } catch { return null; }
111
+ }
112
+
113
+ // ─── Step 1: Non-embeddable Kotlin compiler ────────────────────────────
114
+
115
+ async function setupKotlinCompiler() {
116
+ const compilerJar = path.join(outDir, `kotlin-compiler-${KOTLIN_VERSION}.jar`);
117
+ if (fs.existsSync(compilerJar)) {
118
+ console.log(`✓ kotlin-compiler-${KOTLIN_VERSION}.jar (already exists)`);
119
+ return compilerJar;
120
+ }
121
+
122
+ fs.mkdirSync(outDir, { recursive: true });
123
+ const url = `https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-compiler/${KOTLIN_VERSION}/kotlin-compiler-${KOTLIN_VERSION}.jar`;
124
+ await download(url, compilerJar);
125
+ const size = fs.statSync(compilerJar).size;
126
+ console.log(`✓ kotlin-compiler-${KOTLIN_VERSION}.jar (${(size / 1024 / 1024).toFixed(1)}MB)`);
127
+ return compilerJar;
128
+ }
129
+
130
+ // ─── Step 2: Compose compiler plugin ───────────────────────────────────
131
+
132
+ function setupComposePlugin() {
133
+ const plugin = findInGradleCache(
134
+ 'org.jetbrains.kotlin',
135
+ 'kotlin-compose-compiler-plugin',
136
+ '*.jar'
137
+ );
138
+ if (plugin) {
139
+ console.log(`✓ Compose compiler plugin (from Gradle cache)`);
140
+ return plugin;
141
+ }
142
+ console.warn('⚠ Compose compiler plugin not found in Gradle cache.');
143
+ console.warn(' Run an Android build first: npx expo run:android');
144
+ return null;
145
+ }
146
+
147
+ // ─── Step 3: Compose AAR classes.jar ───────────────────────────────────
148
+
149
+ function setupComposeJars() {
150
+ fs.mkdirSync(composeJarsDir, { recursive: true });
151
+
152
+ const aars = [
153
+ ['androidx.compose.runtime', `runtime-android`],
154
+ ['androidx.compose.runtime', `runtime-saveable-android`],
155
+ ['androidx.compose.ui', `ui-android`],
156
+ ['androidx.compose.foundation', `foundation-android`],
157
+ ['androidx.compose.foundation', `foundation-layout-android`],
158
+ ['androidx.compose.material3', `material3-android`],
159
+ ['androidx.lifecycle', 'lifecycle-common'],
160
+ ['androidx.lifecycle', 'lifecycle-runtime'],
161
+ ['androidx.savedstate', 'savedstate'],
162
+ ];
163
+
164
+ let found = 0;
165
+ for (const [group, artifact] of aars) {
166
+ const jar = findAarClassesJar(group, artifact);
167
+ if (jar) {
168
+ found++;
169
+ } else {
170
+ // Try plain JAR (some are not AARs)
171
+ const plainJar = findInGradleCache(group, artifact);
172
+ if (plainJar) {
173
+ const dest = path.join(composeJarsDir, `${artifact}.jar`);
174
+ if (!fs.existsSync(dest)) fs.copyFileSync(plainJar, dest);
175
+ found++;
176
+ }
177
+ }
178
+ }
179
+
180
+ console.log(`✓ Compose AAR classes.jar (${found}/${aars.length} found)`);
181
+ if (found < aars.length) {
182
+ console.warn(' Some JARs missing. Run an Android build first: npx expo run:android');
183
+ }
184
+ }
185
+
186
+ // ─── Step 4: Pre-transform stubs ───────────────────────────────────────
187
+
188
+ function setupPretransformJar() {
189
+ const ptJar = path.join(outDir, `compose-pretransform-${COMPOSE_VERSION}.jar`);
190
+ if (fs.existsSync(ptJar)) {
191
+ console.log(`✓ compose-pretransform-${COMPOSE_VERSION}.jar (already exists)`);
192
+ return;
193
+ }
194
+
195
+ // The pretransform JAR contains stubs for inline Compose functions
196
+ // (remember, currentComposer) that the standalone kotlinc needs.
197
+ // These are minimal type-resolution stubs, not full implementations.
198
+ const srcDir = path.join(outDir, 'src');
199
+ fs.mkdirSync(srcDir, { recursive: true });
200
+
201
+ // Minimal remember stub
202
+ fs.writeFileSync(path.join(srcDir, 'Composables.kt'), `
203
+ package androidx.compose.runtime
204
+
205
+ @Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE, AnnotationTarget.TYPE_PARAMETER, AnnotationTarget.PROPERTY_GETTER)
206
+ @Retention(AnnotationRetention.BINARY)
207
+ annotation class Composable
208
+
209
+ inline fun <T> remember(crossinline calculation: @Composable () -> T): T = calculation()
210
+
211
+ @Composable
212
+ fun currentComposer(): Any? = null
213
+ `);
214
+
215
+ // Try to compile with the Kotlin compiler
216
+ const kotlinStdlib = findInGradleCache('org.jetbrains.kotlin', 'kotlin-stdlib', null, KOTLIN_VERSION);
217
+ if (!kotlinStdlib) {
218
+ console.warn('⚠ Cannot build pretransform JAR — kotlin-stdlib not in Gradle cache');
219
+ return;
220
+ }
221
+
222
+ const fullCompiler = path.join(outDir, `kotlin-compiler-${KOTLIN_VERSION}.jar`);
223
+ if (!fs.existsSync(fullCompiler)) {
224
+ console.warn('⚠ Cannot build pretransform JAR — kotlin-compiler not downloaded yet');
225
+ return;
226
+ }
227
+
228
+ try {
229
+ const jvmCp = [
230
+ fullCompiler,
231
+ kotlinStdlib,
232
+ findInGradleCache('org.jetbrains.kotlin', 'kotlin-script-runtime', null, KOTLIN_VERSION),
233
+ findInGradleCache('org.jetbrains.kotlinx', 'kotlinx-coroutines-core-jvm'),
234
+ findInGradleCache('org.jetbrains.intellij.deps', 'trove4j'),
235
+ findInGradleCache('org.jetbrains', 'annotations'),
236
+ ].filter(Boolean);
237
+ const cmd = `java -cp "${jvmCp.join(':')}" org.jetbrains.kotlin.cli.jvm.K2JVMCompiler "${srcDir}/Composables.kt" -d "${ptJar}" -classpath "${kotlinStdlib}" -no-reflect -no-stdlib -jvm-target 17 2>&1`;
238
+ execSync(cmd, { stdio: 'pipe', encoding: 'utf8' });
239
+ console.log(`✓ compose-pretransform-${COMPOSE_VERSION}.jar (built)`);
240
+ } catch (e) {
241
+ console.warn(`⚠ Failed to build pretransform JAR: ${(e.stderr || e.message || '').slice(0, 200)}`);
242
+ }
243
+ }
244
+
245
+ // ─── Step 5: Non-inline wrappers ───────────────────────────────────────
246
+
247
+ function setupWrappersJar() {
248
+ const wrappersJar = path.join(outDir, 'compose-wrappers.jar');
249
+ if (fs.existsSync(wrappersJar)) {
250
+ console.log(`✓ compose-wrappers.jar (already exists)`);
251
+ return;
252
+ }
253
+
254
+ // Non-inline wrappers for Box/Column/Row/Spacer compiled WITH the Compose plugin.
255
+ // These delegate to the real implementations without inline body conflicts.
256
+ const srcDir = path.join(outDir, 'wrapper-src');
257
+ fs.mkdirSync(srcDir, { recursive: true });
258
+
259
+ fs.writeFileSync(path.join(srcDir, 'ComposeWrappers.kt'), `
260
+ package com.nativfabric.compose
261
+
262
+ import androidx.compose.runtime.Composable
263
+ import androidx.compose.foundation.layout.BoxScope
264
+ import androidx.compose.foundation.layout.ColumnScope
265
+ import androidx.compose.foundation.layout.RowScope
266
+ import androidx.compose.ui.Alignment
267
+ import androidx.compose.ui.Modifier
268
+
269
+ @Composable
270
+ fun Box(
271
+ modifier: Modifier = Modifier,
272
+ contentAlignment: Alignment = Alignment.TopStart,
273
+ propagateMinConstraints: Boolean = false,
274
+ content: @Composable BoxScope.() -> Unit
275
+ ) = androidx.compose.foundation.layout.Box(modifier, contentAlignment, propagateMinConstraints, content)
276
+
277
+ @Composable
278
+ fun Column(
279
+ modifier: Modifier = Modifier,
280
+ content: @Composable ColumnScope.() -> Unit
281
+ ) = androidx.compose.foundation.layout.Column(modifier = modifier, content = content)
282
+
283
+ @Composable
284
+ fun Row(
285
+ modifier: Modifier = Modifier,
286
+ content: @Composable RowScope.() -> Unit
287
+ ) = androidx.compose.foundation.layout.Row(modifier = modifier, content = content)
288
+
289
+ @Composable
290
+ fun Spacer(modifier: Modifier = Modifier) = androidx.compose.foundation.layout.Spacer(modifier)
291
+ `);
292
+
293
+ // This needs the Compose plugin + all Compose JARs to compile.
294
+ // Skip if deps aren't available — it'll be built on first Android build.
295
+ const plugin = setupComposePlugin();
296
+ const fullCompiler = path.join(outDir, `kotlin-compiler-${KOTLIN_VERSION}.jar`);
297
+ const kotlinStdlib = findInGradleCache('org.jetbrains.kotlin', 'kotlin-stdlib', null, KOTLIN_VERSION);
298
+
299
+ if (!plugin || !fs.existsSync(fullCompiler) || !kotlinStdlib) {
300
+ console.warn('⚠ Cannot build compose-wrappers.jar — missing deps. Run Android build first.');
301
+ return;
302
+ }
303
+
304
+ const cp = [kotlinStdlib];
305
+ // Add android.jar for Android types
306
+ const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT
307
+ || path.join(process.env.HOME || '', 'Library/Android/sdk');
308
+ const wrapperPlatformsDir = path.join(androidHome, 'platforms');
309
+ if (fs.existsSync(wrapperPlatformsDir)) {
310
+ const platforms = fs.readdirSync(wrapperPlatformsDir).sort();
311
+ if (platforms.length > 0) {
312
+ const jar = path.join(wrapperPlatformsDir, platforms[platforms.length - 1], 'android.jar');
313
+ if (fs.existsSync(jar)) cp.push(jar);
314
+ }
315
+ }
316
+ const localAndroidJar = path.join(process.cwd(), '.nativ/kotlin-cache/android.jar');
317
+ if (!cp.some(p => p.includes('android.jar')) && fs.existsSync(localAndroidJar)) cp.push(localAndroidJar);
318
+
319
+ // Add all Compose JARs to classpath
320
+ try {
321
+ const jars = fs.readdirSync(composeJarsDir).filter(f => f.endsWith('.jar')).map(f => path.join(composeJarsDir, f));
322
+ cp.push(...jars);
323
+ } catch {}
324
+
325
+ // Add pretransform JAR
326
+ const ptJar = path.join(outDir, `compose-pretransform-${COMPOSE_VERSION}.jar`);
327
+ if (fs.existsSync(ptJar)) cp.unshift(ptJar);
328
+
329
+ const jvmDeps = [
330
+ fullCompiler,
331
+ kotlinStdlib,
332
+ findInGradleCache('org.jetbrains.kotlin', 'kotlin-script-runtime', null, KOTLIN_VERSION),
333
+ findInGradleCache('org.jetbrains.kotlinx', 'kotlinx-coroutines-core-jvm'),
334
+ findInGradleCache('org.jetbrains.intellij.deps', 'trove4j'),
335
+ findInGradleCache('org.jetbrains', 'annotations'),
336
+ ].filter(Boolean);
337
+
338
+ try {
339
+ const cmd = [
340
+ `java -cp "${jvmDeps.join(':')}" org.jetbrains.kotlin.cli.jvm.K2JVMCompiler`,
341
+ `"${srcDir}/ComposeWrappers.kt"`,
342
+ `-d "${wrappersJar}"`,
343
+ `-classpath "${cp.join(':')}"`,
344
+ `-Xplugin=${plugin}`,
345
+ `-no-reflect -no-stdlib -jvm-target 17`,
346
+ ].join(' ');
347
+ execSync(cmd, { stdio: 'pipe', encoding: 'utf8' });
348
+ console.log(`✓ compose-wrappers.jar (built)`);
349
+ } catch (e) {
350
+ console.warn(`⚠ Failed to build compose-wrappers.jar: ${(e.stderr || e.message || '').slice(0, 200)}`);
351
+ console.warn(' This is expected on first setup. Run an Android build, then re-run this script.');
352
+ }
353
+ }
354
+
355
+ // ─── Step 6: ComposeHost JAR ───────────────────────────────────────────
356
+
357
+ function setupComposeHostJar() {
358
+ const hostJar = path.join(outDir, 'compose-host.jar');
359
+ if (fs.existsSync(hostJar)) {
360
+ console.log(`✓ compose-host.jar (already exists)`);
361
+ return;
362
+ }
363
+
364
+ // ComposeHost wraps ComposeView.setContent for hot-reloaded .dex components.
365
+ // Previously required a full Gradle build — now compiled standalone.
366
+ const srcDir = path.join(outDir, 'host-src');
367
+ fs.mkdirSync(srcDir, { recursive: true });
368
+
369
+ fs.writeFileSync(path.join(srcDir, 'ComposeHost.kt'), `
370
+ package com.nativfabric
371
+
372
+ import android.view.ViewGroup
373
+ import android.widget.FrameLayout
374
+ import androidx.compose.runtime.Composable
375
+ import androidx.compose.ui.platform.ComposeView
376
+
377
+ object ComposeHost {
378
+ @JvmStatic
379
+ fun setContent(parent: ViewGroup, content: @Composable () -> Unit) {
380
+ val composeView = ComposeView(parent.context)
381
+ composeView.setContent { content() }
382
+ parent.addView(composeView, FrameLayout.LayoutParams(
383
+ FrameLayout.LayoutParams.MATCH_PARENT,
384
+ FrameLayout.LayoutParams.MATCH_PARENT))
385
+ }
386
+ }
387
+ `);
388
+
389
+ const plugin = setupComposePlugin();
390
+ const fullCompiler = path.join(outDir, `kotlin-compiler-${KOTLIN_VERSION}.jar`);
391
+ const kotlinStdlib = findInGradleCache('org.jetbrains.kotlin', 'kotlin-stdlib', null, KOTLIN_VERSION);
392
+
393
+ if (!plugin || !fs.existsSync(fullCompiler) || !kotlinStdlib) {
394
+ console.warn('⚠ Cannot build compose-host.jar — missing deps');
395
+ return;
396
+ }
397
+
398
+ const cp = [kotlinStdlib];
399
+ // Add android.jar for Android types (ViewGroup, FrameLayout, Context)
400
+ const androidHome = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT
401
+ || path.join(process.env.HOME || '', 'Library/Android/sdk');
402
+ const platformsDir = path.join(androidHome, 'platforms');
403
+ if (fs.existsSync(platformsDir)) {
404
+ const platforms = fs.readdirSync(platformsDir).sort();
405
+ if (platforms.length > 0) {
406
+ const jar = path.join(platformsDir, platforms[platforms.length - 1], 'android.jar');
407
+ if (fs.existsSync(jar)) cp.push(jar);
408
+ }
409
+ }
410
+ // Fall back to local cache
411
+ const localAndroidJar = path.join(process.cwd(), '.nativ/kotlin-cache/android.jar');
412
+ if (!cp.some(p => p.includes('android.jar')) && fs.existsSync(localAndroidJar)) cp.push(localAndroidJar);
413
+
414
+ // Add pretransform + wrappers JARs for Compose type resolution
415
+ const ptJar = path.join(outDir, `compose-pretransform-${COMPOSE_VERSION}.jar`);
416
+ if (fs.existsSync(ptJar)) cp.unshift(ptJar);
417
+ const wrappersJar = path.join(outDir, 'compose-wrappers.jar');
418
+ if (fs.existsSync(wrappersJar)) cp.unshift(wrappersJar);
419
+ try {
420
+ const jars = fs.readdirSync(composeJarsDir).filter(f => f.endsWith('.jar')).map(f => path.join(composeJarsDir, f));
421
+ cp.push(...jars);
422
+ } catch {}
423
+
424
+ const jvmDeps = [
425
+ fullCompiler,
426
+ kotlinStdlib,
427
+ findInGradleCache('org.jetbrains.kotlin', 'kotlin-script-runtime', null, KOTLIN_VERSION),
428
+ findInGradleCache('org.jetbrains.kotlinx', 'kotlinx-coroutines-core-jvm'),
429
+ findInGradleCache('org.jetbrains.intellij.deps', 'trove4j'),
430
+ findInGradleCache('org.jetbrains', 'annotations'),
431
+ ].filter(Boolean);
432
+
433
+ try {
434
+ const cmd = [
435
+ `java -cp "${jvmDeps.join(':')}" org.jetbrains.kotlin.cli.jvm.K2JVMCompiler`,
436
+ `"${srcDir}/ComposeHost.kt"`,
437
+ `-d "${hostJar}"`,
438
+ `-classpath "${cp.join(':')}"`,
439
+ `-Xplugin=${plugin}`,
440
+ `-no-reflect -jvm-target 17`,
441
+ ].join(' ');
442
+ execSync(cmd, { stdio: 'pipe', encoding: 'utf8' });
443
+ console.log(`✓ compose-host.jar (built)`);
444
+ } catch (e) {
445
+ console.warn(`⚠ Failed to build compose-host.jar: ${(e.stderr || e.message || '').slice(0, 200)}`);
446
+ }
447
+ }
448
+
449
+ // ─── Main ──────────────────────────────────────────────────────────────
450
+
451
+ async function main() {
452
+ console.log('React Native Native — Kotlin/Compose dev toolchain setup\n');
453
+
454
+ await setupKotlinCompiler();
455
+ setupComposePlugin();
456
+ setupComposeJars();
457
+ setupPretransformJar();
458
+ setupWrappersJar();
459
+ setupComposeHostJar();
460
+
461
+ console.log('\nDone. Compose hot-reload is ready.');
462
+ }
463
+
464
+ main().catch(e => {
465
+ console.error('Setup failed:', e.message);
466
+ process.exit(1);
467
+ });