craft-native 0.0.202 → 0.0.203

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.
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Generates native Android apps from web content using WebView.
5
5
  */
6
+ export { runKotlinFixture } from './testing.js';
6
7
  /** Oldest Android API level supported by the generated Kotlin runtime. */
7
8
  export declare const ANDROID_MIN_SDK = 26;
8
9
  export interface CraftAndroidConfig {
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  // ../android/src/index.ts
3
- import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
4
- import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "path";
3
+ import { cpSync, existsSync, mkdirSync, readFileSync as readFileSync2, rmSync, statSync, writeFileSync } from "fs";
4
+ import { dirname as dirname2, extname, isAbsolute, join as join2, relative, resolve, sep } from "path";
5
5
  var {$ } = globalThis.Bun;
6
6
 
7
7
  // ../android/src/promise-runtime.ts
@@ -77,17 +77,73 @@ function renderAndroidPromiseRuntime(indent = "") {
77
77
  `);
78
78
  }
79
79
 
80
+ // ../android/src/testing.ts
81
+ import { readFileSync } from "fs";
82
+ import { mkdtemp, rm, writeFile } from "fs/promises";
83
+ import { tmpdir } from "os";
84
+ import { dirname, join } from "path";
85
+ async function runKotlinFixture(source) {
86
+ const gradle = Bun.which("gradle");
87
+ if (!gradle)
88
+ throw new Error("Gradle is required to run native Kotlin fixtures");
89
+ const template = readFileSync(join(dirname(import.meta.dir), "templates/build.gradle.kts.project.template"), "utf8");
90
+ const version = template.match(/id\("org.jetbrains.kotlin.android"\) version "([\d.]+)"/)?.[1];
91
+ if (!version)
92
+ throw new Error("The Android builder Kotlin version is missing");
93
+ const directory = await mkdtemp(join(tmpdir(), "craft-kotlin-fixture-"));
94
+ try {
95
+ await writeFile(join(directory, "Fixture.kt"), source);
96
+ await writeFile(join(directory, "settings.gradle"), `rootProject.name = 'craft-kotlin-fixture'
97
+ `);
98
+ await writeFile(join(directory, "gradle.properties"), `org.gradle.jvmargs=-Xmx512m
99
+ `);
100
+ await writeFile(join(directory, "build.gradle"), `
101
+ repositories { mavenCentral() }
102
+ configurations {
103
+ kotlinCompiler
104
+ kotlinRuntime
105
+ }
106
+ dependencies {
107
+ kotlinCompiler 'org.jetbrains.kotlin:kotlin-compiler-embeddable:${version}'
108
+ kotlinRuntime 'org.jetbrains.kotlin:kotlin-stdlib:${version}'
109
+ }
110
+ tasks.register('compileFixture', JavaExec) {
111
+ classpath = configurations.kotlinCompiler
112
+ mainClass = 'org.jetbrains.kotlin.cli.jvm.K2JVMCompiler'
113
+ args '-no-stdlib', '-no-reflect', '-classpath', configurations.kotlinRuntime.asPath,
114
+ '-d', layout.buildDirectory.dir('classes').get().asFile.absolutePath,
115
+ file('Fixture.kt').absolutePath
116
+ }
117
+ tasks.register('runFixture', JavaExec) {
118
+ dependsOn 'compileFixture'
119
+ classpath = files(layout.buildDirectory.dir('classes')) + configurations.kotlinRuntime
120
+ mainClass = 'FixtureKt'
121
+ }
122
+ `);
123
+ const child = Bun.spawn([gradle, "--no-daemon", "--console=plain", "--max-workers=2", "runFixture"], {
124
+ cwd: directory,
125
+ stdout: "inherit",
126
+ stderr: "inherit"
127
+ });
128
+ const code = await child.exited;
129
+ if (code !== 0)
130
+ throw new Error(`Native Kotlin fixture failed with exit code ${code}`);
131
+ } finally {
132
+ await rm(directory, { recursive: true, force: true });
133
+ }
134
+ }
135
+
80
136
  // ../android/src/index.ts
81
- var TEMPLATES_DIR = join(dirname(import.meta.dir), "templates");
137
+ var TEMPLATES_DIR = join2(dirname2(import.meta.dir), "templates");
82
138
  function resolveCraftAndroidVersion(startDir = import.meta.dir) {
83
139
  let directory = resolve(startDir);
84
140
  while (true) {
85
141
  try {
86
- const pkg = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
142
+ const pkg = JSON.parse(readFileSync2(join2(directory, "package.json"), "utf8"));
87
143
  if ((pkg.name === "@craft-native/android" || pkg.name === "craft-native") && typeof pkg.version === "string" && pkg.version)
88
144
  return pkg.version;
89
145
  } catch {}
90
- const parent = dirname(directory);
146
+ const parent = dirname2(directory);
91
147
  if (parent === directory)
92
148
  return "0.0.0";
93
149
  directory = parent;
@@ -226,7 +282,7 @@ function validateGoogleServicesFile(path, packageName) {
226
282
  requireRegularFile(path, "Google services file");
227
283
  let document;
228
284
  try {
229
- document = JSON.parse(readFileSync(path, "utf8"));
285
+ document = JSON.parse(readFileSync2(path, "utf8"));
230
286
  } catch (error) {
231
287
  throw new Error(`Google services file must contain valid JSON: ${path}`, { cause: error });
232
288
  }
@@ -325,11 +381,11 @@ function validateAndroidConfig(config) {
325
381
  }
326
382
  }
327
383
  function writeAndroidConfig(output, config) {
328
- writeFileSync(join(output, "craft.config.json"), JSON.stringify(config, null, 2));
384
+ writeFileSync(join2(output, "craft.config.json"), JSON.stringify(config, null, 2));
329
385
  const runtimeConfig = { ...config };
330
386
  delete runtimeConfig.appIconPath;
331
387
  delete runtimeConfig.googleServicesFile;
332
- writeFileSync(join(output, "app/src/main/assets/craft.config.json"), JSON.stringify(runtimeConfig, null, 2));
388
+ writeFileSync(join2(output, "app/src/main/assets/craft.config.json"), JSON.stringify(runtimeConfig, null, 2));
333
389
  }
334
390
  function syncAndroidWebAssets(source, output) {
335
391
  const sourcePath = resolve(source);
@@ -344,17 +400,17 @@ function syncAndroidWebAssets(source, output) {
344
400
  throw new Error(`Web asset source must not overlap generated asset directory: ${source}`);
345
401
  }
346
402
  if (sourceStat.isDirectory()) {
347
- requireRegularFile(join(sourcePath, "index.html"), "Web asset directory entry point");
403
+ requireRegularFile(join2(sourcePath, "index.html"), "Web asset directory entry point");
348
404
  }
349
- const configPath = join(assetsDir, "craft.config.json");
350
- const config = existsSync(configPath) ? readFileSync(configPath) : undefined;
405
+ const configPath = join2(assetsDir, "craft.config.json");
406
+ const config = existsSync(configPath) ? readFileSync2(configPath) : undefined;
351
407
  rmSync(assetsDir, { recursive: true, force: true });
352
408
  mkdirSync(assetsDir, { recursive: true });
353
409
  if (sourceStat.isDirectory())
354
410
  cpSync(sourcePath, assetsDir, { recursive: true });
355
411
  else
356
- cpSync(sourcePath, join(assetsDir, "index.html"));
357
- if (!existsSync(join(assetsDir, "index.html")))
412
+ cpSync(sourcePath, join2(assetsDir, "index.html"));
413
+ if (!existsSync(join2(assetsDir, "index.html")))
358
414
  throw new Error(`Web asset directory must contain index.html: ${source}`);
359
415
  if (config)
360
416
  writeFileSync(configPath, config);
@@ -398,10 +454,10 @@ function renderAndroidPermissions(config) {
398
454
  `);
399
455
  }
400
456
  function writeNativeShellSources(output, packagePath, packageName, config) {
401
- const javaDir = join(output, "app/src/main/java", packagePath);
457
+ const javaDir = join2(output, "app/src/main/java", packagePath);
402
458
  const write = (template, file, replace = (source) => source) => {
403
- const source = readFileSync(join(TEMPLATES_DIR, template), "utf-8").replace(/\{\{PACKAGE_NAME\}\}/g, packageName);
404
- writeFileSync(join(javaDir, file), replace(source));
459
+ const source = readFileSync2(join2(TEMPLATES_DIR, template), "utf-8").replace(/\{\{PACKAGE_NAME\}\}/g, packageName);
460
+ writeFileSync(join2(javaDir, file), replace(source));
405
461
  };
406
462
  write("CraftBluetooth.kt.template", "CraftBluetooth.kt");
407
463
  write("CraftNativeShell.kt.template", "CraftNativeShell.kt");
@@ -422,9 +478,9 @@ function writeNativeShellSources(output, packagePath, packageName, config) {
422
478
  if (config.enablePushNotifications)
423
479
  write("CraftNativeMessagingService.kt.template", "CraftNativeMessagingService.kt");
424
480
  else
425
- rmSync(join(javaDir, "CraftNativeMessagingService.kt"), { force: true });
426
- mkdirSync(join(output, "app/src/main/res/xml"), { recursive: true });
427
- writeFileSync(join(output, "app/src/main/res/xml/craft_file_paths.xml"), `<?xml version="1.0" encoding="utf-8"?>
481
+ rmSync(join2(javaDir, "CraftNativeMessagingService.kt"), { force: true });
482
+ mkdirSync(join2(output, "app/src/main/res/xml"), { recursive: true });
483
+ writeFileSync(join2(output, "app/src/main/res/xml/craft_file_paths.xml"), `<?xml version="1.0" encoding="utf-8"?>
428
484
  <paths>
429
485
  <cache-path name="craft-files" path="craft-files/" />
430
486
  <files-path name="craft-media" path="craft-media/" />
@@ -503,7 +559,7 @@ function resolveRuntimeDir(override) {
503
559
  return dir;
504
560
  }
505
561
  function installRuntime(output, runtimeDir) {
506
- const found = RUNTIME_ABIS.map((abi) => ({ abi, source: join(runtimeDir, abi, "libcraft.so") })).filter((entry) => existsSync(entry.source));
562
+ const found = RUNTIME_ABIS.map((abi) => ({ abi, source: join2(runtimeDir, abi, "libcraft.so") })).filter((entry) => existsSync(entry.source));
507
563
  if (found.length === 0) {
508
564
  throw new Error(`${runtimeDir} has no <abi>/libcraft.so for any of ${RUNTIME_ABIS.join(", ")}. ` + "Run `zig build build-android-all -Doptimize=ReleaseSafe` in packages/zig and point at its zig-out/android.");
509
565
  }
@@ -511,12 +567,12 @@ function installRuntime(output, runtimeDir) {
511
567
  const missing = RUNTIME_ABIS.filter((abi) => !found.some((entry) => entry.abi === abi));
512
568
  console.warn(` \u26A0 only ${found.map((entry) => entry.abi).join(", ")} was found; ${missing.join(", ")} is missing. ` + "The app will fall back to the Kotlin shim on those devices.");
513
569
  }
514
- const dest = join(output, "app/src/main/jniLibs");
570
+ const dest = join2(output, "app/src/main/jniLibs");
515
571
  rmSync(dest, { force: true, recursive: true });
516
572
  for (const { abi, source } of found) {
517
- const abiDir = join(dest, abi);
573
+ const abiDir = join2(dest, abi);
518
574
  mkdirSync(abiDir, { recursive: true });
519
- cpSync(source, join(abiDir, "libcraft.so"));
575
+ cpSync(source, join2(abiDir, "libcraft.so"));
520
576
  }
521
577
  return true;
522
578
  }
@@ -558,13 +614,13 @@ async function init(options) {
558
614
  }
559
615
  const dirs = [
560
616
  output,
561
- join(output, "app/src/main/java", packagePath),
562
- join(output, "app/src/main/res/layout"),
563
- join(output, "app/src/main/res/values"),
564
- join(output, "app/src/main/res/values-night"),
565
- join(output, "app/src/main/res/drawable"),
566
- join(output, "app/src/main/assets"),
567
- join(output, "gradle/wrapper")
617
+ join2(output, "app/src/main/java", packagePath),
618
+ join2(output, "app/src/main/res/layout"),
619
+ join2(output, "app/src/main/res/values"),
620
+ join2(output, "app/src/main/res/values-night"),
621
+ join2(output, "app/src/main/res/drawable"),
622
+ join2(output, "app/src/main/assets"),
623
+ join2(output, "gradle/wrapper")
568
624
  ];
569
625
  if (existsSync(output) && !statSync(output).isDirectory()) {
570
626
  throw new Error(`Android project output must be a directory: ${output}`);
@@ -577,27 +633,27 @@ async function init(options) {
577
633
  writeAndroidConfig(output, config);
578
634
  const hasGoogleServices = Boolean(config.googleServicesFile);
579
635
  if (config.googleServicesFile) {
580
- cpSync(config.googleServicesFile, join(output, "app/google-services.json"));
636
+ cpSync(config.googleServicesFile, join2(output, "app/google-services.json"));
581
637
  }
582
- const mainActivityTemplate = readFileSync(join(TEMPLATES_DIR, config.renderer === "native" ? "MainActivityNative.kt.template" : "MainActivity.kt.template"), "utf-8");
638
+ const mainActivityTemplate = readFileSync2(join2(TEMPLATES_DIR, config.renderer === "native" ? "MainActivityNative.kt.template" : "MainActivity.kt.template"), "utf-8");
583
639
  const mainActivity = mainActivityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
584
- writeFileSync(join(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
585
- writeFileSync(join(output, "app/src/main/java", packagePath, "CraftAppLinks.kt"), readFileSync(join(TEMPLATES_DIR, "CraftAppLinks.kt.template"), "utf8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
586
- writeFileSync(join(output, "app/src/main/java", packagePath, "CraftCrashReporter.kt"), readFileSync(join(TEMPLATES_DIR, "CraftCrashReporter.kt.template"), "utf-8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_ANDROID_VERSION));
640
+ writeFileSync(join2(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
641
+ writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftAppLinks.kt"), readFileSync2(join2(TEMPLATES_DIR, "CraftAppLinks.kt.template"), "utf8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
642
+ writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftCrashReporter.kt"), readFileSync2(join2(TEMPLATES_DIR, "CraftCrashReporter.kt.template"), "utf-8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_ANDROID_VERSION));
587
643
  if (config.renderer === "native") {
588
- const mutationTemplate = readFileSync(join(TEMPLATES_DIR, "CraftNativeMutation.kt.template"), "utf-8");
589
- const flatListTemplate = readFileSync(join(TEMPLATES_DIR, "CraftNativeFlatList.kt.template"), "utf-8");
590
- const refreshableFlatListTemplate = readFileSync(join(TEMPLATES_DIR, "CraftNativeRefreshableFlatList.kt.template"), "utf-8");
591
- const capabilityTemplate = readFileSync(join(TEMPLATES_DIR, "CraftNativeCapabilities.kt.template"), "utf-8");
592
- const notificationTemplate = readFileSync(join(TEMPLATES_DIR, "CraftNativeNotificationReceiver.kt.template"), "utf-8");
593
- writeFileSync(join(output, "app/src/main/java", packagePath, "CraftNativeMutation.kt"), mutationTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
594
- writeFileSync(join(output, "app/src/main/java", packagePath, "CraftNativeFlatList.kt"), flatListTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
595
- writeFileSync(join(output, "app/src/main/java", packagePath, "CraftNativeRefreshableFlatList.kt"), refreshableFlatListTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
596
- writeFileSync(join(output, "app/src/main/java", packagePath, "CraftNativeCapabilities.kt"), capabilityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
597
- writeFileSync(join(output, "app/src/main/java", packagePath, "CraftNativeNotificationReceiver.kt"), notificationTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
644
+ const mutationTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftNativeMutation.kt.template"), "utf-8");
645
+ const flatListTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftNativeFlatList.kt.template"), "utf-8");
646
+ const refreshableFlatListTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftNativeRefreshableFlatList.kt.template"), "utf-8");
647
+ const capabilityTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftNativeCapabilities.kt.template"), "utf-8");
648
+ const notificationTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftNativeNotificationReceiver.kt.template"), "utf-8");
649
+ writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftNativeMutation.kt"), mutationTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
650
+ writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftNativeFlatList.kt"), flatListTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
651
+ writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftNativeRefreshableFlatList.kt"), refreshableFlatListTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
652
+ writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftNativeCapabilities.kt"), capabilityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
653
+ writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftNativeNotificationReceiver.kt"), notificationTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
598
654
  writeNativeShellSources(output, packagePath, finalPackageName, config);
599
655
  }
600
- const craftBridgeTemplate = readFileSync(join(TEMPLATES_DIR, "CraftBridge.kt.template"), "utf-8");
656
+ const craftBridgeTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftBridge.kt.template"), "utf-8");
601
657
  const craftBridge = craftBridgeTemplate.replace(/\{\{PROMISE_RUNTIME\}\}/g, () => renderAndroidPromiseRuntime(" ")).replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{ENABLE_SPEECH\}\}/g, String(Boolean(config.enableSpeechRecognition))).replace(/\{\{ENABLE_HAPTICS\}\}/g, String(Boolean(config.enableHaptics))).replace(/\{\{ENABLE_SHARE\}\}/g, String(Boolean(config.enableShare))).replace(/\{\{ENABLE_CAMERA\}\}/g, String(Boolean(config.enableCamera))).replace(/\{\{ENABLE_BIOMETRIC\}\}/g, String(Boolean(config.enableBiometric))).replace(/\{\{ENABLE_PUSH\}\}/g, String(Boolean(config.enablePushNotifications))).replace(/\{\{ENABLE_SECURE_STORAGE\}\}/g, String(Boolean(config.enableSecureStorage))).replace(/\{\{ENABLE_GEOLOCATION\}\}/g, String(Boolean(config.enableGeolocation))).replace(/\{\{ENABLE_BACKGROUND_LOCATION\}\}/g, String(Boolean(config.enableBackgroundLocation))).replace(/\{\{ENABLE_KEEP_AWAKE\}\}/g, String(Boolean(config.enableKeepAwake))).replace(/\{\{ENABLE_DEEP_LINKS\}\}/g, String(Boolean(config.enableDeepLinks))).replace(/\{\{ENABLE_HEALTH_CONNECT\}\}/g, String(Boolean(config.enableHealthConnect))).replace(/\{\{FIREBASE_IMPORT\}\}/g, config.enablePushNotifications ? "import com.google.firebase.messaging.FirebaseMessaging" : "").replace(/\{\{REGISTER_PUSH_IMPLEMENTATION\}\}/g, config.enablePushNotifications ? `activity.runOnUiThread {
602
658
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
603
659
  && ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
@@ -617,16 +673,16 @@ async function init(options) {
617
673
  }` : `evaluatePromiseJavascript(
618
674
  "window._craftPushReject && window._craftPushReject('Push notifications are disabled')"
619
675
  )`);
620
- writeFileSync(join(output, "app/src/main/java", packagePath, "CraftBridge.kt"), craftBridge);
621
- const craftNative = readFileSync(join(TEMPLATES_DIR, "CraftNative.kt.template"), "utf-8");
622
- const nativeDir = join(output, "app/src/main/java/com/craft/runtime");
676
+ writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftBridge.kt"), craftBridge);
677
+ const craftNative = readFileSync2(join2(TEMPLATES_DIR, "CraftNative.kt.template"), "utf-8");
678
+ const nativeDir = join2(output, "app/src/main/java/com/craft/runtime");
623
679
  mkdirSync(nativeDir, { recursive: true });
624
- writeFileSync(join(nativeDir, "CraftNative.kt"), craftNative);
625
- const serviceTemplate = readFileSync(join(TEMPLATES_DIR, "LocationRecordingService.kt.template"), "utf-8");
626
- writeFileSync(join(nativeDir, "LocationRecordingService.kt"), serviceTemplate);
627
- const healthTemplate = readFileSync(join(TEMPLATES_DIR, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
628
- writeFileSync(join(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
629
- const manifestTemplate = readFileSync(join(TEMPLATES_DIR, "AndroidManifest.xml.template"), "utf-8");
680
+ writeFileSync(join2(nativeDir, "CraftNative.kt"), craftNative);
681
+ const serviceTemplate = readFileSync2(join2(TEMPLATES_DIR, "LocationRecordingService.kt.template"), "utf-8");
682
+ writeFileSync(join2(nativeDir, "LocationRecordingService.kt"), serviceTemplate);
683
+ const healthTemplate = readFileSync2(join2(TEMPLATES_DIR, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
684
+ writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
685
+ const manifestTemplate = readFileSync2(join2(TEMPLATES_DIR, "AndroidManifest.xml.template"), "utf-8");
630
686
  const manifest = manifestTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{PERMISSIONS\}\}/g, renderAndroidPermissions(config)).replace(/\{\{USES_CLEARTEXT\}\}/g, config.devServerURL?.startsWith("http://") || config.enableLocalNetwork ? "true" : "false").replace(/\{\{DEEP_LINK_INTENT_FILTERS\}\}/g, renderAndroidDeepLinks(config)).replace(/\{\{BACKGROUND_SERVICE\}\}/g, config.enableBackgroundLocation ? ' <service android:name="com.craft.runtime.LocationRecordingService" android:exported="false" android:foregroundServiceType="location" android:stopWithTask="false" />' : "").replace(/\{\{NATIVE_NOTIFICATION_RECEIVER\}\}/g, config.renderer === "native" ? renderNativeShellManifest(config) : "").replace(/\{\{CRASH_REPORTER_PROVIDER\}\}/g, ` <provider android:name=".CraftCrashReporterInit" android:authorities="\${applicationId}.craft-crash" android:exported="false" android:initOrder="1000" />`).replace(/\{\{GOOGLE_MAPS_API_KEY\}\}/g, config.mapsApiKey ? ` <meta-data android:name="com.google.android.geo.API_KEY" android:value="${escapeXml(config.mapsApiKey)}" />` : "").replace(/\{\{HEALTH_CONNECT_QUERIES\}\}/g, config.enableHealthConnect ? ` <queries>
631
687
  <package android:name="com.google.android.apps.healthdata" />
632
688
  </queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
@@ -636,29 +692,28 @@ async function init(options) {
636
692
  <action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
637
693
  <category android:name="android.intent.category.HEALTH_PERMISSIONS" />
638
694
  </intent-filter>`}` : "");
639
- writeFileSync(join(output, "app/src/main/AndroidManifest.xml"), manifest);
640
- const projectGradleTemplate = readFileSync(join(TEMPLATES_DIR, "build.gradle.kts.project.template"), "utf-8");
641
- writeFileSync(join(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
642
- const appGradleTemplate = readFileSync(join(TEMPLATES_DIR, "build.gradle.kts.app.template"), "utf-8");
695
+ writeFileSync(join2(output, "app/src/main/AndroidManifest.xml"), manifest);
696
+ const projectGradleTemplate = readFileSync2(join2(TEMPLATES_DIR, "build.gradle.kts.project.template"), "utf-8");
697
+ writeFileSync(join2(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
698
+ const appGradleTemplate = readFileSync2(join2(TEMPLATES_DIR, "build.gradle.kts.app.template"), "utf-8");
643
699
  const appGradle = appGradleTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{VERSION_NAME\}\}/g, escapeKotlinString(config.version || "1.0.0")).replace(/\{\{VERSION_CODE\}\}/g, String(config.versionCode || 1)).replace(/\{\{MIN_SDK\}\}/g, String(config.minSdk ?? ANDROID_MIN_SDK)).replace(/\{\{COMPILE_SDK\}\}/g, String(config.compileSdk || 36)).replace(/\{\{TARGET_SDK\}\}/g, String(config.targetSdk || 35)).replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services")' : "").replace(/\{\{FIREBASE_MESSAGING_DEPENDENCY\}\}/g, config.enablePushNotifications ? ' implementation("com.google.firebase:firebase-messaging:24.1.0")' : "").replace(/\{\{REFRESH_LAYOUT_DEPENDENCY\}\}/g, config.renderer === "native" ? ' implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")' : "").replace(/\{\{GOOGLE_MAPS_DEPENDENCY\}\}/g, config.renderer === "native" ? ` // Native STX Map overlays and trail routes
644
700
  implementation("com.google.android.gms:play-services-maps:18.2.0")` : "").replace(/\{\{HEALTH_CONNECT_DEPENDENCIES\}\}/g, config.enableHealthConnect ? ` implementation("androidx.health.connect:connect-client:1.1.0")
645
701
  implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")` : "").replace(/\{\{JAVASCRIPT_ENGINE_DEPENDENCY\}\}/g, config.renderer === "native" ? ` implementation("androidx.javascriptengine:javascriptengine:1.1.0")
646
- // The in-app browser (Custom Tabs) and background refresh (WorkManager).
647
- implementation("androidx.browser:browser:1.8.0")
702
+ // Native background refresh (WorkManager).
648
703
  implementation("androidx.work:work-runtime-ktx:2.9.1")` : "");
649
- writeFileSync(join(output, "app/build.gradle.kts"), appGradle);
650
- const proguardTemplate = readFileSync(join(TEMPLATES_DIR, "proguard-rules.pro.template"), "utf-8");
651
- writeFileSync(join(output, "app/proguard-rules.pro"), proguardTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
652
- const settingsTemplate = readFileSync(join(TEMPLATES_DIR, "settings.gradle.kts.template"), "utf-8");
704
+ writeFileSync(join2(output, "app/build.gradle.kts"), appGradle);
705
+ const proguardTemplate = readFileSync2(join2(TEMPLATES_DIR, "proguard-rules.pro.template"), "utf-8");
706
+ writeFileSync(join2(output, "app/proguard-rules.pro"), proguardTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
707
+ const settingsTemplate = readFileSync2(join2(TEMPLATES_DIR, "settings.gradle.kts.template"), "utf-8");
653
708
  const settings = settingsTemplate.replace(/\{\{APP_NAME\}\}/g, escapeKotlinString(generatedGradleProjectName(name)));
654
- writeFileSync(join(output, "settings.gradle.kts"), settings);
709
+ writeFileSync(join2(output, "settings.gradle.kts"), settings);
655
710
  const gradleProperties = `org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
656
711
  android.useAndroidX=true
657
712
  kotlin.code.style=official
658
713
  android.nonTransitiveRClass=true
659
714
  `;
660
- writeFileSync(join(output, "gradle.properties"), gradleProperties);
661
- writeFileSync(join(output, "local.properties"), `# SDK location will be set by Android Studio
715
+ writeFileSync(join2(output, "gradle.properties"), gradleProperties);
716
+ writeFileSync(join2(output, "local.properties"), `# SDK location will be set by Android Studio
662
717
  `);
663
718
  const gradleWrapperProps = `distributionBase=GRADLE_USER_HOME
664
719
  distributionPath=wrapper/dists
@@ -668,13 +723,13 @@ validateDistributionUrl=true
668
723
  zipStoreBase=GRADLE_USER_HOME
669
724
  zipStorePath=wrapper/dists
670
725
  `;
671
- writeFileSync(join(output, "gradle/wrapper/gradle-wrapper.properties"), gradleWrapperProps);
726
+ writeFileSync(join2(output, "gradle/wrapper/gradle-wrapper.properties"), gradleWrapperProps);
672
727
  const stringsXml = `<?xml version="1.0" encoding="utf-8"?>
673
728
  <resources>
674
729
  <string name="app_name">${escapeXml(name)}</string>
675
730
  </resources>
676
731
  `;
677
- writeFileSync(join(output, "app/src/main/res/values/strings.xml"), stringsXml);
732
+ writeFileSync(join2(output, "app/src/main/res/values/strings.xml"), stringsXml);
678
733
  const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
679
734
  <resources>
680
735
  <color name="primary">#1a1a2e</color>
@@ -683,8 +738,8 @@ zipStorePath=wrapper/dists
683
738
  <color name="background">${config.backgroundColor}</color>
684
739
  </resources>
685
740
  `;
686
- writeFileSync(join(output, "app/src/main/res/values/colors.xml"), colorsXml);
687
- const darkColorsPath = join(output, "app/src/main/res/values-night/colors.xml");
741
+ writeFileSync(join2(output, "app/src/main/res/values/colors.xml"), colorsXml);
742
+ const darkColorsPath = join2(output, "app/src/main/res/values-night/colors.xml");
688
743
  if (config.backgroundColorDark) {
689
744
  writeFileSync(darkColorsPath, `<?xml version="1.0" encoding="utf-8"?>
690
745
  <resources>
@@ -705,9 +760,9 @@ zipStorePath=wrapper/dists
705
760
  </vector>
706
761
  `;
707
762
  if (config.appIconPath) {
708
- cpSync(config.appIconPath, join(output, `app/src/main/res/drawable/craft_app_icon${appIconExtension}`));
763
+ cpSync(config.appIconPath, join2(output, `app/src/main/res/drawable/craft_app_icon${appIconExtension}`));
709
764
  } else {
710
- writeFileSync(join(output, "app/src/main/res/drawable/craft_app_icon.xml"), appIconXml);
765
+ writeFileSync(join2(output, "app/src/main/res/drawable/craft_app_icon.xml"), appIconXml);
711
766
  }
712
767
  const themesXml = `<?xml version="1.0" encoding="utf-8"?>
713
768
  <resources>
@@ -718,7 +773,7 @@ zipStorePath=wrapper/dists
718
773
  </style>
719
774
  </resources>
720
775
  `;
721
- writeFileSync(join(output, "app/src/main/res/values/themes.xml"), themesXml);
776
+ writeFileSync(join2(output, "app/src/main/res/values/themes.xml"), themesXml);
722
777
  const activityMainXml = config.renderer === "native" ? `<?xml version="1.0" encoding="utf-8"?>
723
778
  <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
724
779
  android:id="@+id/native_root"
@@ -738,7 +793,7 @@ zipStorePath=wrapper/dists
738
793
 
739
794
  </androidx.coordinatorlayout.widget.CoordinatorLayout>
740
795
  `;
741
- writeFileSync(join(output, "app/src/main/res/layout/activity_main.xml"), activityMainXml);
796
+ writeFileSync(join2(output, "app/src/main/res/layout/activity_main.xml"), activityMainXml);
742
797
  const placeholderHtml = `<!DOCTYPE html>
743
798
  <html>
744
799
  <head>
@@ -776,7 +831,7 @@ zipStorePath=wrapper/dists
776
831
  </script>
777
832
  </body>
778
833
  </html>`;
779
- writeFileSync(join(output, "app/src/main/assets/index.html"), placeholderHtml);
834
+ writeFileSync(join2(output, "app/src/main/assets/index.html"), placeholderHtml);
780
835
  const runtimeDir = resolveRuntimeDir(options.runtimeDir);
781
836
  if (runtimeDir) {
782
837
  installRuntime(output, runtimeDir);
@@ -800,19 +855,19 @@ async function build(options) {
800
855
  const { htmlPath, nativeBundlePath, devServer, output, release, bundle, compile = true } = options;
801
856
  console.log(`
802
857
  \uD83D\uDCE6 Building Craft Android project...`);
803
- const configPath = join(output, "craft.config.json");
858
+ const configPath = join2(output, "craft.config.json");
804
859
  if (!existsSync(configPath)) {
805
860
  throw new Error(`No craft.config.json found in ${output}. Run 'craft android init' first.`);
806
861
  }
807
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
862
+ const config = JSON.parse(readFileSync2(configPath, "utf-8"));
808
863
  validateAndroidConfig(config);
809
864
  if (config.renderer === "native") {
810
865
  if (htmlPath || devServer)
811
866
  throw new Error("A native Android screen cannot use --html-path or --dev-server.");
812
- const destination = join(output, "app/src/main/assets/native-screen.js");
867
+ const destination = join2(output, "app/src/main/assets/native-screen.js");
813
868
  if (nativeBundlePath && resolve(nativeBundlePath) !== resolve(destination))
814
869
  cpSync(nativeBundlePath, destination);
815
- const icons = join(output, "app/src/main/assets/craft-icons.json");
870
+ const icons = join2(output, "app/src/main/assets/craft-icons.json");
816
871
  if (options.iconsPath) {
817
872
  requireRegularFile(options.iconsPath, "Icon table");
818
873
  if (resolve(options.iconsPath) !== resolve(icons))
@@ -824,7 +879,7 @@ async function build(options) {
824
879
  } else if (nativeBundlePath) {
825
880
  throw new Error('--native-bundle requires renderer: "native" in craft.config.json.');
826
881
  }
827
- if (existsSync(join(output, "app/src/main/jniLibs"))) {
882
+ if (existsSync(join2(output, "app/src/main/jniLibs"))) {
828
883
  const runtimeDir = resolveRuntimeDir(options.runtimeDir);
829
884
  if (runtimeDir) {
830
885
  installRuntime(output, runtimeDir);
@@ -851,7 +906,7 @@ async function build(options) {
851
906
  }
852
907
  if (!compile)
853
908
  return;
854
- const gradlewPath = join(output, "gradlew");
909
+ const gradlewPath = join2(output, "gradlew");
855
910
  if (!existsSync(gradlewPath)) {
856
911
  console.log(" Generating Gradle wrapper...");
857
912
  try {
@@ -898,13 +953,13 @@ async function run(options) {
898
953
  await build({ output });
899
954
  console.log("\uD83D\uDCF1 Installing and running on device...");
900
955
  try {
901
- const apkPath = join(output, "app/build/outputs/apk/debug/app-debug.apk");
956
+ const apkPath = join2(output, "app/build/outputs/apk/debug/app-debug.apk");
902
957
  if (device) {
903
958
  await $`adb -s ${device} install -r ${apkPath}`;
904
959
  } else {
905
960
  await $`adb install -r ${apkPath}`;
906
961
  }
907
- const config = JSON.parse(readFileSync(join(output, "craft.config.json"), "utf-8"));
962
+ const config = JSON.parse(readFileSync2(join2(output, "craft.config.json"), "utf-8"));
908
963
  const launchCmd = `${config.packageName}/.MainActivity`;
909
964
  if (device) {
910
965
  await $`adb -s ${device} shell am start -n ${launchCmd}`;
@@ -921,6 +976,7 @@ async function run(options) {
921
976
  }
922
977
  export {
923
978
  syncAndroidWebAssets,
979
+ runKotlinFixture,
924
980
  run,
925
981
  resolveRuntimeDir,
926
982
  renderAndroidPermissions,
@@ -0,0 +1,2 @@
1
+ /** Uses the Android builder's Kotlin version, including its transitive compiler dependencies. */
2
+ export declare function runKotlinFixture(source: string): Promise<void>;