craft-native 0.0.236 → 0.0.238

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 (34) hide show
  1. package/README.md +71 -0
  2. package/dist/android/src/emulator.d.ts +23 -0
  3. package/dist/android/src/index.d.ts +7 -1
  4. package/dist/android/src/index.js +280 -98
  5. package/dist/android/templates/CraftBridge.kt.template +65 -54
  6. package/dist/android/templates/CraftNative.kt.template +27 -0
  7. package/dist/android/templates/CraftNativeCapabilities.kt.template +55 -6
  8. package/dist/android/templates/CraftNativeServices.kt.template +45 -35
  9. package/dist/android/templates/CraftNativeShell.kt.template +103 -20
  10. package/dist/android/templates/LocationRecordingService.kt.template +252 -46
  11. package/dist/android/templates/MainActivityNative.kt.template +163 -5
  12. package/dist/android-adb.d.ts +2 -0
  13. package/dist/api/index.d.ts +1 -1
  14. package/dist/api/mobile.d.ts +49 -14
  15. package/dist/browser/storage.d.ts +3 -0
  16. package/dist/browser.js +163 -51
  17. package/dist/cli.js +1 -1
  18. package/dist/components/index.d.ts +33 -0
  19. package/dist/index.cjs +126 -12
  20. package/dist/index.js +126 -12
  21. package/dist/ios/src/app-shortcuts.d.ts +11 -0
  22. package/dist/ios/src/index.d.ts +7 -0
  23. package/dist/ios/src/index.js +65 -1
  24. package/dist/ios/src/testing.d.ts +2 -0
  25. package/dist/ios/templates/CraftApp.swift +392 -88
  26. package/dist/ios/templates/CraftHeartRate.swift +141 -44
  27. package/dist/ios/templates/CraftNativeActions.swift +46 -48
  28. package/dist/ios/templates/CraftNativeScreen.swift +177 -10
  29. package/dist/ios/templates/CraftPersistentStorage.swift +99 -0
  30. package/dist/ios/templates/CraftRecordingArchive.swift +85 -0
  31. package/dist/ios/templates/testing/DriverTests.swift.template +8 -1
  32. package/dist/mobile-testing.js +30 -4
  33. package/dist/mobile.js +169 -17
  34. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  // ../android/src/index.ts
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";
3
+ import { cpSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync, writeFileSync } from "fs";
4
+ import { dirname as dirname2, extname, isAbsolute as isAbsolute2, join as join3, relative, resolve as resolve2, sep } from "path";
5
5
  var {$ } = globalThis.Bun;
6
6
 
7
7
  // ../android/src/promise-runtime.ts
@@ -132,10 +132,191 @@ tasks.register('runFixture', JavaExec) {
132
132
  await rm(directory, { recursive: true, force: true });
133
133
  }
134
134
  }
135
+ // ../android/src/emulator.ts
136
+ import { appendFileSync, closeSync, mkdirSync, mkdtempSync, openSync, rmSync } from "fs";
137
+ import { availableParallelism, cpus, tmpdir as tmpdir2, totalmem } from "os";
138
+ import { basename, isAbsolute, join as join2, resolve } from "path";
139
+ async function createAndroidEmulator(options) {
140
+ if (!["darwin", "linux"].includes(process.platform))
141
+ throw new Error("Android emulator sessions require macOS or Linux");
142
+ if (!["hardware", "software"].includes(options.acceleration))
143
+ throw new Error("Select hardware or software Android emulation explicitly");
144
+ if (![options.sdkmanager, options.avdmanager].every(isAbsolute))
145
+ throw new Error("Select absolute Android command-line tool paths");
146
+ const abi = process.arch === "arm64" ? "arm64-v8a" : process.arch === "x64" ? "x86_64" : undefined;
147
+ if (!abi || !/^system-images;android-\d+;[^;\s]+;[^;\s]+$/.test(options.image) || !options.image.endsWith(`;${abi}`))
148
+ throw new Error("The Android system image must match the host architecture");
149
+ if (options.acceleration === "software" && process.arch !== "x64")
150
+ throw new Error("Android software emulation requires an x86_64 host and system image");
151
+ const bootTimeoutMs = options.bootTimeoutMs ?? 600000;
152
+ const maximumBootTimeoutMs = options.acceleration === "software" ? 1200000 : 600000;
153
+ if (!Number.isSafeInteger(bootTimeoutMs) || bootTimeoutMs < 1000 || bootTimeoutMs > maximumBootTimeoutMs)
154
+ throw new Error(`Android bootTimeoutMs must be an integer between 1000 and ${maximumBootTimeoutMs} milliseconds`);
155
+ const port = options.port ?? 5558;
156
+ if (!Number.isInteger(port) || port < 5554 || port > 5682 || port % 2 !== 0)
157
+ throw new Error("Android console ports must be even and between 5554 and 5682");
158
+ const sdk = resolve(options.sdkRoot);
159
+ const artifacts = resolve(options.artifactsDirectory);
160
+ const temporary = options.temporaryDirectory ?? tmpdir2();
161
+ mkdirSync(artifacts, { recursive: true });
162
+ mkdirSync(temporary, { recursive: true });
163
+ const owned = mkdtempSync(join2(temporary, "craft-android-emulator-"));
164
+ const name = basename(owned);
165
+ const serial = `emulator-${port}`;
166
+ const env = {
167
+ ...process.env,
168
+ ...options.env,
169
+ ANDROID_HOME: sdk,
170
+ ANDROID_SDK_ROOT: sdk,
171
+ ANDROID_SDK_HOME: owned,
172
+ ANDROID_USER_HOME: join2(owned, ".android"),
173
+ ANDROID_EMULATOR_HOME: join2(owned, ".android"),
174
+ ANDROID_AVD_HOME: join2(owned, "avd"),
175
+ ANDROID_SERIAL: serial,
176
+ AVDMANAGER_OPTS: `'-Dcom.android.sdkmanager.toolsdir=${join2(sdk, "cmdline-tools/latest")}'`
177
+ };
178
+ const adb = join2(sdk, "platform-tools/adb");
179
+ const executable = join2(sdk, "emulator/emulator");
180
+ let child;
181
+ let log;
182
+ let created = false;
183
+ let sequence = 0;
184
+ let disposal;
185
+ let bootStartedAt = 0;
186
+ let cpuStart = 0;
187
+ let idleStart = 0;
188
+ let identity;
189
+ async function recordBootResult(status, endedAt = Date.now()) {
190
+ if (!identity)
191
+ return;
192
+ const cpuEnd = cpus();
193
+ const totalDeltaMs = cpuEnd.reduce((total, cpu) => total + Object.values(cpu.times).reduce((sum, value) => sum + value, 0), 0) - cpuStart;
194
+ const idleDeltaMs = cpuEnd.reduce((total, cpu) => total + cpu.times.idle, 0) - idleStart;
195
+ await Bun.write(join2(artifacts, "android-emulator.json"), JSON.stringify({
196
+ ...identity,
197
+ status,
198
+ [status === "booted" ? "bootedAt" : "failedAt"]: new Date(endedAt).toISOString(),
199
+ bootElapsedMs: endedAt - bootStartedAt,
200
+ hostCpuInterval: { totalDeltaMs, idleDeltaMs, busyDeltaMs: totalDeltaMs - idleDeltaMs }
201
+ }, null, 2) + `
202
+ `);
203
+ }
204
+ async function run(args, input, timeout = 900000, allowFailure = false) {
205
+ const startedAt = new Date().toISOString();
206
+ const command = Bun.spawn(args, { env, stdin: input === undefined ? "ignore" : new TextEncoder().encode(input), stdout: "pipe", stderr: "pipe", signal: AbortSignal.timeout(timeout) });
207
+ const [code, stdout, stderr] = await Promise.all([command.exited, new Response(command.stdout).text(), new Response(command.stderr).text()]);
208
+ appendFileSync(join2(artifacts, "device-commands.jsonl"), JSON.stringify({ sequence: ++sequence, startedAt, finishedAt: new Date().toISOString(), args, code, stdout, stderr }) + `
209
+ `);
210
+ if (code !== 0 && !allowFailure)
211
+ throw new Error(`${basename(args[0])} failed with exit ${code}; inspect ${artifacts}`);
212
+ return { code, stdout };
213
+ }
214
+ const dispose = () => disposal ??= (async () => {
215
+ try {
216
+ if (child && child.exitCode === null) {
217
+ child.kill("SIGTERM");
218
+ await Promise.race([child.exited, Bun.sleep(1e4)]);
219
+ if (child.exitCode === null)
220
+ child.kill("SIGKILL");
221
+ await child.exited;
222
+ }
223
+ if (created)
224
+ await run([options.avdmanager, "delete", "avd", "--name", name], undefined, 30000);
225
+ } finally {
226
+ if (log !== undefined)
227
+ closeSync(log);
228
+ rmSync(owned, { recursive: true, force: true });
229
+ }
230
+ })();
231
+ try {
232
+ mkdirSync(env.ANDROID_AVD_HOME, { recursive: true });
233
+ mkdirSync(env.ANDROID_USER_HOME, { recursive: true });
234
+ if (options.acceptLicenses)
235
+ await run([options.sdkmanager, `--sdk_root=${sdk}`, "--licenses"], `y
236
+ `.repeat(100));
237
+ await run([options.sdkmanager, `--sdk_root=${sdk}`, "platform-tools", "emulator"], options.acceptLicenses ? `y
238
+ `.repeat(100) : undefined);
239
+ const acceleration = await run([executable, "-accel-check"], undefined, 30000, true);
240
+ if (options.acceleration === "hardware" && acceleration.code !== 0)
241
+ throw new Error(`Android hardware acceleration is unavailable; inspect ${artifacts}`);
242
+ await run([options.sdkmanager, `--sdk_root=${sdk}`, options.image], options.acceptLicenses ? `y
243
+ `.repeat(100) : undefined);
244
+ const devices = (await run([adb, "devices"], undefined, 1e4)).stdout;
245
+ if (devices.split(`
246
+ `).some((line) => line.startsWith(`${serial} `)))
247
+ throw new Error(`${serial} is already in use`);
248
+ await run([options.avdmanager, "create", "avd", "--name", name, "--package", options.image, "--device", "pixel_6"], `no
249
+ `);
250
+ created = true;
251
+ log = openSync(join2(artifacts, "emulator.log"), "w");
252
+ bootStartedAt = Date.now();
253
+ const hostCpus = cpus();
254
+ cpuStart = hostCpus.reduce((total, cpu) => total + Object.values(cpu.times).reduce((sum, value) => sum + value, 0), 0);
255
+ idleStart = hostCpus.reduce((total, cpu) => total + cpu.times.idle, 0);
256
+ child = Bun.spawn([executable, "-avd", name, "-port", String(port), "-no-window", "-no-snapshot", "-no-audio", "-no-boot-anim", "-gpu", "software", "-memory", "2048", "-cores", "2", "-accel", options.acceleration === "software" ? "off" : "on"], { env, stdout: log, stderr: log });
257
+ identity = {
258
+ image: options.image,
259
+ acceleration: options.acceleration,
260
+ accelerationProbeExitCode: acceleration.code,
261
+ bootTimeoutMs,
262
+ bootStartedAt: new Date(bootStartedAt).toISOString(),
263
+ host: { cpuModel: hostCpus[0]?.model, logicalCpus: hostCpus.length, availableParallelism: availableParallelism(), memoryBytes: totalmem() },
264
+ virtualCpus: 2,
265
+ memoryMb: 2048,
266
+ serial,
267
+ avdDirectory: env.ANDROID_AVD_HOME,
268
+ emulatorPid: child.pid
269
+ };
270
+ await Bun.write(join2(artifacts, "android-emulator.json"), JSON.stringify(identity, null, 2) + `
271
+ `);
272
+ const deadline = bootStartedAt + bootTimeoutMs;
273
+ const probeTimeout = () => Math.max(1, Math.min(1e4, deadline - Date.now()));
274
+ while (Date.now() < deadline && child.exitCode === null) {
275
+ let booted = false;
276
+ const connection = await run([adb, "devices"], undefined, probeTimeout(), true);
277
+ if (connection.code === 0 && connection.stdout.includes(`${serial} device`)) {
278
+ const completion = await run([adb, "-s", serial, "shell", "getprop", "sys.boot_completed"], undefined, probeTimeout(), true);
279
+ if (completion.code === 0 && completion.stdout.trim() === "1") {
280
+ const settings = await run([adb, "-s", serial, "shell", "settings", "get", "global", "animator_duration_scale"], undefined, probeTimeout(), true);
281
+ const packages = await run([adb, "-s", serial, "shell", "pm", "path", "android"], undefined, probeTimeout(), true);
282
+ const systemUi = await run([adb, "-s", serial, "shell", "pidof", "com.android.systemui"], undefined, probeTimeout(), true);
283
+ const events = await run([adb, "-s", serial, "logcat", "-b", "events", "-d", "-t", "1"], undefined, probeTimeout(), true);
284
+ booted = settings.code === 0 && packages.code === 0 && systemUi.code === 0 && events.code === 0 && /^package:\/\S+/m.test(packages.stdout) && /^\d+(?:\s+\d+)*$/.test(systemUi.stdout.trim());
285
+ }
286
+ }
287
+ const checkedAt = Date.now();
288
+ if (booted && checkedAt <= deadline) {
289
+ await recordBootResult("booted", checkedAt);
290
+ return { serial, adb, env, dispose };
291
+ }
292
+ await Bun.sleep(Math.min(2000, Math.max(0, deadline - Date.now())));
293
+ }
294
+ throw new Error(`The owned Android emulator did not boot within ${bootTimeoutMs}ms; inspect ${artifacts}`);
295
+ } catch (error) {
296
+ if (child) {
297
+ try {
298
+ await recordBootResult("failed");
299
+ } catch (diagnostic) {
300
+ console.error(`Could not retain emulator boot timing: ${diagnostic}`);
301
+ }
302
+ for (const args of [["logcat", "-d", "-b", "all"], ["shell", "getprop"], ["shell", "dumpsys", "activity", "processes"]]) {
303
+ try {
304
+ await run([adb, "-s", serial, ...args], undefined, 1e4, true);
305
+ } catch {}
306
+ }
307
+ }
308
+ try {
309
+ await dispose();
310
+ } catch (cleanup) {
311
+ throw new AggregateError([error, cleanup], "Android emulator setup and cleanup failed");
312
+ }
313
+ throw error;
314
+ }
315
+ }
135
316
 
136
317
  // ../android/src/index.ts
137
318
  function syncNativeFonts(fonts, output) {
138
- const destination = join2(output, "app/src/main/assets/NativeFonts");
319
+ const destination = join3(output, "app/src/main/assets/NativeFonts");
139
320
  const names = new Set;
140
321
  const files = Object.entries(fonts ?? {}).map(([family, entry]) => {
141
322
  if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(family))
@@ -143,7 +324,7 @@ function syncNativeFonts(fonts, output) {
143
324
  if (names.has(family.toLowerCase()))
144
325
  throw new Error(`Duplicate native font name: ${family}`);
145
326
  names.add(family.toLowerCase());
146
- if ([entry.src, entry.license].some((file) => file && resolve(file).startsWith(resolve(destination) + sep)))
327
+ if ([entry.src, entry.license].some((file) => file && resolve2(file).startsWith(resolve2(destination) + sep)))
147
328
  throw new Error("Native font sources must be outside the generated NativeFonts directory");
148
329
  const extension = extname(entry.src).toLowerCase();
149
330
  if (![".ttf", ".otf"].includes(extension) || !existsSync(entry.src) || !statSync(entry.src).isFile())
@@ -152,23 +333,23 @@ function syncNativeFonts(fonts, output) {
152
333
  throw new Error(`Native font license not found: ${entry.license}`);
153
334
  return { family, ...entry, filename: `${family}${extension}` };
154
335
  });
155
- rmSync(destination, { recursive: true, force: true });
336
+ rmSync2(destination, { recursive: true, force: true });
156
337
  if (!files.length)
157
338
  return [];
158
- mkdirSync(destination, { recursive: true });
339
+ mkdirSync2(destination, { recursive: true });
159
340
  for (const file of files) {
160
- cpSync(file.src, join2(destination, file.filename));
341
+ cpSync(file.src, join3(destination, file.filename));
161
342
  if (file.license)
162
- cpSync(file.license, join2(destination, `${file.family}.LICENSE.txt`));
343
+ cpSync(file.license, join3(destination, `${file.family}.LICENSE.txt`));
163
344
  }
164
345
  return files.map((file) => `NativeFonts/${file.filename}`);
165
346
  }
166
- var TEMPLATES_DIR = join2(dirname2(import.meta.dir), "templates");
347
+ var TEMPLATES_DIR = join3(dirname2(import.meta.dir), "templates");
167
348
  function resolveCraftAndroidVersion(startDir = import.meta.dir) {
168
- let directory = resolve(startDir);
349
+ let directory = resolve2(startDir);
169
350
  while (true) {
170
351
  try {
171
- const pkg = JSON.parse(readFileSync2(join2(directory, "package.json"), "utf8"));
352
+ const pkg = JSON.parse(readFileSync2(join3(directory, "package.json"), "utf8"));
172
353
  if ((pkg.name === "@craft-native/android" || pkg.name === "craft-native") && typeof pkg.version === "string" && pkg.version)
173
354
  return pkg.version;
174
355
  } catch {}
@@ -305,7 +486,7 @@ function requireRegularFile(path, label) {
305
486
  }
306
487
  function containsPath(parent, candidate) {
307
488
  const relativePath = relative(parent, candidate);
308
- return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
489
+ return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute2(relativePath);
309
490
  }
310
491
  function validateGoogleServicesFile(path, packageName) {
311
492
  requireRegularFile(path, "Google services file");
@@ -498,36 +679,36 @@ function validateNativeRouteConfig(config) {
498
679
  }
499
680
  }
500
681
  function writeAndroidConfig(output, config) {
501
- writeFileSync(join2(output, "craft.config.json"), JSON.stringify(config, null, 2));
682
+ writeFileSync(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
502
683
  const runtimeConfig = { ...config };
503
684
  delete runtimeConfig.appIconPath;
504
685
  delete runtimeConfig.googleServicesFile;
505
- writeFileSync(join2(output, "app/src/main/assets/craft.config.json"), JSON.stringify(runtimeConfig, null, 2));
686
+ writeFileSync(join3(output, "app/src/main/assets/craft.config.json"), JSON.stringify(runtimeConfig, null, 2));
506
687
  }
507
688
  function syncAndroidWebAssets(source, output) {
508
- const sourcePath = resolve(source);
689
+ const sourcePath = resolve2(source);
509
690
  if (!existsSync(sourcePath))
510
691
  throw new Error(`Web asset path not found: ${source}`);
511
692
  const sourceStat = statSync(sourcePath);
512
693
  if (!sourceStat.isDirectory() && !sourceStat.isFile()) {
513
694
  throw new Error(`Web asset path must be a file or directory: ${source}`);
514
695
  }
515
- const assetsDir = resolve(output, "app/src/main/assets");
696
+ const assetsDir = resolve2(output, "app/src/main/assets");
516
697
  if (containsPath(sourcePath, assetsDir) || containsPath(assetsDir, sourcePath)) {
517
698
  throw new Error(`Web asset source must not overlap generated asset directory: ${source}`);
518
699
  }
519
700
  if (sourceStat.isDirectory()) {
520
- requireRegularFile(join2(sourcePath, "index.html"), "Web asset directory entry point");
701
+ requireRegularFile(join3(sourcePath, "index.html"), "Web asset directory entry point");
521
702
  }
522
- const configPath = join2(assetsDir, "craft.config.json");
703
+ const configPath = join3(assetsDir, "craft.config.json");
523
704
  const config = existsSync(configPath) ? readFileSync2(configPath) : undefined;
524
- rmSync(assetsDir, { recursive: true, force: true });
525
- mkdirSync(assetsDir, { recursive: true });
705
+ rmSync2(assetsDir, { recursive: true, force: true });
706
+ mkdirSync2(assetsDir, { recursive: true });
526
707
  if (sourceStat.isDirectory())
527
708
  cpSync(sourcePath, assetsDir, { recursive: true });
528
709
  else
529
- cpSync(sourcePath, join2(assetsDir, "index.html"));
530
- if (!existsSync(join2(assetsDir, "index.html")))
710
+ cpSync(sourcePath, join3(assetsDir, "index.html"));
711
+ if (!existsSync(join3(assetsDir, "index.html")))
531
712
  throw new Error(`Web asset directory must contain index.html: ${source}`);
532
713
  if (config)
533
714
  writeFileSync(configPath, config);
@@ -571,10 +752,10 @@ function renderAndroidPermissions(config) {
571
752
  `);
572
753
  }
573
754
  function writeNativeShellSources(output, packagePath, packageName, config) {
574
- const javaDir = join2(output, "app/src/main/java", packagePath);
755
+ const javaDir = join3(output, "app/src/main/java", packagePath);
575
756
  const write = (template, file, replace = (source) => source) => {
576
- const source = readFileSync2(join2(TEMPLATES_DIR, template), "utf-8").replace(/\{\{PACKAGE_NAME\}\}/g, packageName);
577
- writeFileSync(join2(javaDir, file), replace(source));
757
+ const source = readFileSync2(join3(TEMPLATES_DIR, template), "utf-8").replace(/\{\{PACKAGE_NAME\}\}/g, packageName);
758
+ writeFileSync(join3(javaDir, file), replace(source));
578
759
  };
579
760
  write("CraftBluetooth.kt.template", "CraftBluetooth.kt");
580
761
  write("CraftNativeShell.kt.template", "CraftNativeShell.kt");
@@ -596,9 +777,9 @@ function writeNativeShellSources(output, packagePath, packageName, config) {
596
777
  if (config.enablePushNotifications)
597
778
  write("CraftNativeMessagingService.kt.template", "CraftNativeMessagingService.kt");
598
779
  else
599
- rmSync(join2(javaDir, "CraftNativeMessagingService.kt"), { force: true });
600
- mkdirSync(join2(output, "app/src/main/res/xml"), { recursive: true });
601
- writeFileSync(join2(output, "app/src/main/res/xml/craft_file_paths.xml"), `<?xml version="1.0" encoding="utf-8"?>
780
+ rmSync2(join3(javaDir, "CraftNativeMessagingService.kt"), { force: true });
781
+ mkdirSync2(join3(output, "app/src/main/res/xml"), { recursive: true });
782
+ writeFileSync(join3(output, "app/src/main/res/xml/craft_file_paths.xml"), `<?xml version="1.0" encoding="utf-8"?>
602
783
  <paths>
603
784
  <cache-path name="craft-files" path="craft-files/" />
604
785
  <files-path name="craft-media" path="craft-media/" />
@@ -677,7 +858,7 @@ function resolveRuntimeDir(override) {
677
858
  return dir;
678
859
  }
679
860
  function installRuntime(output, runtimeDir) {
680
- const found = RUNTIME_ABIS.map((abi) => ({ abi, source: join2(runtimeDir, abi, "libcraft.so") })).filter((entry) => existsSync(entry.source));
861
+ const found = RUNTIME_ABIS.map((abi) => ({ abi, source: join3(runtimeDir, abi, "libcraft.so") })).filter((entry) => existsSync(entry.source));
681
862
  if (found.length === 0) {
682
863
  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.");
683
864
  }
@@ -685,12 +866,12 @@ function installRuntime(output, runtimeDir) {
685
866
  const missing = RUNTIME_ABIS.filter((abi) => !found.some((entry) => entry.abi === abi));
686
867
  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.");
687
868
  }
688
- const dest = join2(output, "app/src/main/jniLibs");
689
- rmSync(dest, { force: true, recursive: true });
869
+ const dest = join3(output, "app/src/main/jniLibs");
870
+ rmSync2(dest, { force: true, recursive: true });
690
871
  for (const { abi, source } of found) {
691
- const abiDir = join2(dest, abi);
692
- mkdirSync(abiDir, { recursive: true });
693
- cpSync(source, join2(abiDir, "libcraft.so"));
872
+ const abiDir = join3(dest, abi);
873
+ mkdirSync2(abiDir, { recursive: true });
874
+ cpSync(source, join3(abiDir, "libcraft.so"));
694
875
  }
695
876
  return true;
696
877
  }
@@ -733,46 +914,46 @@ async function init(options) {
733
914
  }
734
915
  const dirs = [
735
916
  output,
736
- join2(output, "app/src/main/java", packagePath),
737
- join2(output, "app/src/main/res/layout"),
738
- join2(output, "app/src/main/res/values"),
739
- join2(output, "app/src/main/res/values-night"),
740
- join2(output, "app/src/main/res/drawable"),
741
- join2(output, "app/src/main/assets"),
742
- join2(output, "gradle/wrapper")
917
+ join3(output, "app/src/main/java", packagePath),
918
+ join3(output, "app/src/main/res/layout"),
919
+ join3(output, "app/src/main/res/values"),
920
+ join3(output, "app/src/main/res/values-night"),
921
+ join3(output, "app/src/main/res/drawable"),
922
+ join3(output, "app/src/main/assets"),
923
+ join3(output, "gradle/wrapper")
743
924
  ];
744
925
  if (existsSync(output) && !statSync(output).isDirectory()) {
745
926
  throw new Error(`Android project output must be a directory: ${output}`);
746
927
  }
747
928
  for (const dir of dirs) {
748
929
  if (!existsSync(dir)) {
749
- mkdirSync(dir, { recursive: true });
930
+ mkdirSync2(dir, { recursive: true });
750
931
  }
751
932
  }
752
933
  writeAndroidConfig(output, config);
753
934
  const hasGoogleServices = Boolean(config.googleServicesFile);
754
935
  if (config.googleServicesFile) {
755
- cpSync(config.googleServicesFile, join2(output, "app/google-services.json"));
936
+ cpSync(config.googleServicesFile, join3(output, "app/google-services.json"));
756
937
  }
757
- const mainActivityTemplate = readFileSync2(join2(TEMPLATES_DIR, config.renderer === "native" ? "MainActivityNative.kt.template" : "MainActivity.kt.template"), "utf-8");
938
+ const mainActivityTemplate = readFileSync2(join3(TEMPLATES_DIR, config.renderer === "native" ? "MainActivityNative.kt.template" : "MainActivity.kt.template"), "utf-8");
758
939
  const mainActivity = mainActivityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
759
- writeFileSync(join2(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
760
- writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftAppLinks.kt"), readFileSync2(join2(TEMPLATES_DIR, "CraftAppLinks.kt.template"), "utf8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
761
- 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));
940
+ writeFileSync(join3(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
941
+ writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftAppLinks.kt"), readFileSync2(join3(TEMPLATES_DIR, "CraftAppLinks.kt.template"), "utf8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
942
+ writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftCrashReporter.kt"), readFileSync2(join3(TEMPLATES_DIR, "CraftCrashReporter.kt.template"), "utf-8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_ANDROID_VERSION));
762
943
  if (config.renderer === "native") {
763
- const mutationTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftNativeMutation.kt.template"), "utf-8");
764
- const flatListTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftNativeFlatList.kt.template"), "utf-8");
765
- const refreshableFlatListTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftNativeRefreshableFlatList.kt.template"), "utf-8");
766
- const capabilityTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftNativeCapabilities.kt.template"), "utf-8");
767
- const notificationTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftNativeNotificationReceiver.kt.template"), "utf-8");
768
- writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftNativeMutation.kt"), mutationTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
769
- writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftNativeFlatList.kt"), flatListTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
770
- writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftNativeRefreshableFlatList.kt"), refreshableFlatListTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
771
- writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftNativeCapabilities.kt"), capabilityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
772
- writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftNativeNotificationReceiver.kt"), notificationTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
944
+ const mutationTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftNativeMutation.kt.template"), "utf-8");
945
+ const flatListTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftNativeFlatList.kt.template"), "utf-8");
946
+ const refreshableFlatListTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftNativeRefreshableFlatList.kt.template"), "utf-8");
947
+ const capabilityTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftNativeCapabilities.kt.template"), "utf-8");
948
+ const notificationTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftNativeNotificationReceiver.kt.template"), "utf-8");
949
+ writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftNativeMutation.kt"), mutationTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
950
+ writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftNativeFlatList.kt"), flatListTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
951
+ writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftNativeRefreshableFlatList.kt"), refreshableFlatListTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
952
+ writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftNativeCapabilities.kt"), capabilityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
953
+ writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftNativeNotificationReceiver.kt"), notificationTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
773
954
  writeNativeShellSources(output, packagePath, finalPackageName, config);
774
955
  }
775
- const craftBridgeTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftBridge.kt.template"), "utf-8");
956
+ const craftBridgeTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftBridge.kt.template"), "utf-8");
776
957
  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 {
777
958
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
778
959
  && ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
@@ -792,16 +973,16 @@ async function init(options) {
792
973
  }` : `evaluatePromiseJavascript(
793
974
  "window._craftPushReject && window._craftPushReject('Push notifications are disabled')"
794
975
  )`);
795
- writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftBridge.kt"), craftBridge);
796
- const craftNative = readFileSync2(join2(TEMPLATES_DIR, "CraftNative.kt.template"), "utf-8");
797
- const nativeDir = join2(output, "app/src/main/java/com/craft/runtime");
798
- mkdirSync(nativeDir, { recursive: true });
799
- writeFileSync(join2(nativeDir, "CraftNative.kt"), craftNative);
800
- const serviceTemplate = readFileSync2(join2(TEMPLATES_DIR, "LocationRecordingService.kt.template"), "utf-8");
801
- writeFileSync(join2(nativeDir, "LocationRecordingService.kt"), serviceTemplate);
802
- const healthTemplate = readFileSync2(join2(TEMPLATES_DIR, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
803
- writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
804
- const manifestTemplate = readFileSync2(join2(TEMPLATES_DIR, "AndroidManifest.xml.template"), "utf-8");
976
+ writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftBridge.kt"), craftBridge);
977
+ const craftNative = readFileSync2(join3(TEMPLATES_DIR, "CraftNative.kt.template"), "utf-8");
978
+ const nativeDir = join3(output, "app/src/main/java/com/craft/runtime");
979
+ mkdirSync2(nativeDir, { recursive: true });
980
+ writeFileSync(join3(nativeDir, "CraftNative.kt"), craftNative);
981
+ const serviceTemplate = readFileSync2(join3(TEMPLATES_DIR, "LocationRecordingService.kt.template"), "utf-8");
982
+ writeFileSync(join3(nativeDir, "LocationRecordingService.kt"), serviceTemplate);
983
+ const healthTemplate = readFileSync2(join3(TEMPLATES_DIR, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
984
+ writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
985
+ const manifestTemplate = readFileSync2(join3(TEMPLATES_DIR, "AndroidManifest.xml.template"), "utf-8");
805
986
  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>
806
987
  <package android:name="com.google.android.apps.healthdata" />
807
988
  </queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
@@ -811,28 +992,28 @@ async function init(options) {
811
992
  <action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
812
993
  <category android:name="android.intent.category.HEALTH_PERMISSIONS" />
813
994
  </intent-filter>`}` : "");
814
- writeFileSync(join2(output, "app/src/main/AndroidManifest.xml"), manifest);
815
- const projectGradleTemplate = readFileSync2(join2(TEMPLATES_DIR, "build.gradle.kts.project.template"), "utf-8");
816
- 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' : ""));
817
- const appGradleTemplate = readFileSync2(join2(TEMPLATES_DIR, "build.gradle.kts.app.template"), "utf-8");
995
+ writeFileSync(join3(output, "app/src/main/AndroidManifest.xml"), manifest);
996
+ const projectGradleTemplate = readFileSync2(join3(TEMPLATES_DIR, "build.gradle.kts.project.template"), "utf-8");
997
+ writeFileSync(join3(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
998
+ const appGradleTemplate = readFileSync2(join3(TEMPLATES_DIR, "build.gradle.kts.app.template"), "utf-8");
818
999
  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
819
1000
  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")
820
1001
  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")
821
1002
  // Native background refresh (WorkManager).
822
1003
  implementation("androidx.work:work-runtime-ktx:2.9.1")` : "");
823
- writeFileSync(join2(output, "app/build.gradle.kts"), appGradle);
824
- const proguardTemplate = readFileSync2(join2(TEMPLATES_DIR, "proguard-rules.pro.template"), "utf-8");
825
- writeFileSync(join2(output, "app/proguard-rules.pro"), proguardTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
826
- const settingsTemplate = readFileSync2(join2(TEMPLATES_DIR, "settings.gradle.kts.template"), "utf-8");
1004
+ writeFileSync(join3(output, "app/build.gradle.kts"), appGradle);
1005
+ const proguardTemplate = readFileSync2(join3(TEMPLATES_DIR, "proguard-rules.pro.template"), "utf-8");
1006
+ writeFileSync(join3(output, "app/proguard-rules.pro"), proguardTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
1007
+ const settingsTemplate = readFileSync2(join3(TEMPLATES_DIR, "settings.gradle.kts.template"), "utf-8");
827
1008
  const settings = settingsTemplate.replace(/\{\{APP_NAME\}\}/g, escapeKotlinString(generatedGradleProjectName(name)));
828
- writeFileSync(join2(output, "settings.gradle.kts"), settings);
1009
+ writeFileSync(join3(output, "settings.gradle.kts"), settings);
829
1010
  const gradleProperties = `org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
830
1011
  android.useAndroidX=true
831
1012
  kotlin.code.style=official
832
1013
  android.nonTransitiveRClass=true
833
1014
  `;
834
- writeFileSync(join2(output, "gradle.properties"), gradleProperties);
835
- writeFileSync(join2(output, "local.properties"), `# SDK location will be set by Android Studio
1015
+ writeFileSync(join3(output, "gradle.properties"), gradleProperties);
1016
+ writeFileSync(join3(output, "local.properties"), `# SDK location will be set by Android Studio
836
1017
  `);
837
1018
  const gradleWrapperProps = `distributionBase=GRADLE_USER_HOME
838
1019
  distributionPath=wrapper/dists
@@ -842,13 +1023,13 @@ validateDistributionUrl=true
842
1023
  zipStoreBase=GRADLE_USER_HOME
843
1024
  zipStorePath=wrapper/dists
844
1025
  `;
845
- writeFileSync(join2(output, "gradle/wrapper/gradle-wrapper.properties"), gradleWrapperProps);
1026
+ writeFileSync(join3(output, "gradle/wrapper/gradle-wrapper.properties"), gradleWrapperProps);
846
1027
  const stringsXml = `<?xml version="1.0" encoding="utf-8"?>
847
1028
  <resources>
848
1029
  <string name="app_name">${escapeXml(name)}</string>
849
1030
  </resources>
850
1031
  `;
851
- writeFileSync(join2(output, "app/src/main/res/values/strings.xml"), stringsXml);
1032
+ writeFileSync(join3(output, "app/src/main/res/values/strings.xml"), stringsXml);
852
1033
  const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
853
1034
  <resources>
854
1035
  <color name="primary">#1a1a2e</color>
@@ -857,8 +1038,8 @@ zipStorePath=wrapper/dists
857
1038
  <color name="background">${config.backgroundColor}</color>
858
1039
  </resources>
859
1040
  `;
860
- writeFileSync(join2(output, "app/src/main/res/values/colors.xml"), colorsXml);
861
- const darkColorsPath = join2(output, "app/src/main/res/values-night/colors.xml");
1041
+ writeFileSync(join3(output, "app/src/main/res/values/colors.xml"), colorsXml);
1042
+ const darkColorsPath = join3(output, "app/src/main/res/values-night/colors.xml");
862
1043
  if (config.backgroundColorDark) {
863
1044
  writeFileSync(darkColorsPath, `<?xml version="1.0" encoding="utf-8"?>
864
1045
  <resources>
@@ -866,7 +1047,7 @@ zipStorePath=wrapper/dists
866
1047
  </resources>
867
1048
  `);
868
1049
  } else {
869
- rmSync(darkColorsPath, { force: true });
1050
+ rmSync2(darkColorsPath, { force: true });
870
1051
  }
871
1052
  const appIconXml = `<?xml version="1.0" encoding="utf-8"?>
872
1053
  <vector xmlns:android="http://schemas.android.com/apk/res/android"
@@ -879,9 +1060,9 @@ zipStorePath=wrapper/dists
879
1060
  </vector>
880
1061
  `;
881
1062
  if (config.appIconPath) {
882
- cpSync(config.appIconPath, join2(output, `app/src/main/res/drawable/craft_app_icon${appIconExtension}`));
1063
+ cpSync(config.appIconPath, join3(output, `app/src/main/res/drawable/craft_app_icon${appIconExtension}`));
883
1064
  } else {
884
- writeFileSync(join2(output, "app/src/main/res/drawable/craft_app_icon.xml"), appIconXml);
1065
+ writeFileSync(join3(output, "app/src/main/res/drawable/craft_app_icon.xml"), appIconXml);
885
1066
  }
886
1067
  const themesXml = `<?xml version="1.0" encoding="utf-8"?>
887
1068
  <resources>
@@ -892,7 +1073,7 @@ zipStorePath=wrapper/dists
892
1073
  </style>
893
1074
  </resources>
894
1075
  `;
895
- writeFileSync(join2(output, "app/src/main/res/values/themes.xml"), themesXml);
1076
+ writeFileSync(join3(output, "app/src/main/res/values/themes.xml"), themesXml);
896
1077
  const activityMainXml = config.renderer === "native" ? `<?xml version="1.0" encoding="utf-8"?>
897
1078
  <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
898
1079
  android:id="@+id/native_root"
@@ -912,7 +1093,7 @@ zipStorePath=wrapper/dists
912
1093
 
913
1094
  </androidx.coordinatorlayout.widget.CoordinatorLayout>
914
1095
  `;
915
- writeFileSync(join2(output, "app/src/main/res/layout/activity_main.xml"), activityMainXml);
1096
+ writeFileSync(join3(output, "app/src/main/res/layout/activity_main.xml"), activityMainXml);
916
1097
  syncNativeFonts(config.nativeFonts, output);
917
1098
  const placeholderHtml = `<!DOCTYPE html>
918
1099
  <html>
@@ -951,7 +1132,7 @@ zipStorePath=wrapper/dists
951
1132
  </script>
952
1133
  </body>
953
1134
  </html>`;
954
- writeFileSync(join2(output, "app/src/main/assets/index.html"), placeholderHtml);
1135
+ writeFileSync(join3(output, "app/src/main/assets/index.html"), placeholderHtml);
955
1136
  const runtimeDir = resolveRuntimeDir(options.runtimeDir);
956
1137
  if (runtimeDir) {
957
1138
  installRuntime(output, runtimeDir);
@@ -975,7 +1156,7 @@ async function build(options) {
975
1156
  const { htmlPath, nativeBundlePath, devServer, output, release, bundle, compile = true } = options;
976
1157
  console.log(`
977
1158
  \uD83D\uDCE6 Building Craft Android project...`);
978
- const configPath = join2(output, "craft.config.json");
1159
+ const configPath = join3(output, "craft.config.json");
979
1160
  if (!existsSync(configPath)) {
980
1161
  throw new Error(`No craft.config.json found in ${output}. Run 'craft android init' first.`);
981
1162
  }
@@ -985,13 +1166,13 @@ async function build(options) {
985
1166
  if (config.renderer === "native") {
986
1167
  if (htmlPath || devServer)
987
1168
  throw new Error("A native Android screen cannot use --html-path or --dev-server.");
988
- const destination = join2(output, "app/src/main/assets/native-screen.js");
989
- if (nativeBundlePath && resolve(nativeBundlePath) !== resolve(destination))
1169
+ const destination = join3(output, "app/src/main/assets/native-screen.js");
1170
+ if (nativeBundlePath && resolve2(nativeBundlePath) !== resolve2(destination))
990
1171
  cpSync(nativeBundlePath, destination);
991
- const icons = join2(output, "app/src/main/assets/craft-icons.json");
1172
+ const icons = join3(output, "app/src/main/assets/craft-icons.json");
992
1173
  if (options.iconsPath) {
993
1174
  requireRegularFile(options.iconsPath, "Icon table");
994
- if (resolve(options.iconsPath) !== resolve(icons))
1175
+ if (resolve2(options.iconsPath) !== resolve2(icons))
995
1176
  cpSync(options.iconsPath, icons);
996
1177
  }
997
1178
  if (!existsSync(destination)) {
@@ -1000,7 +1181,7 @@ async function build(options) {
1000
1181
  } else if (nativeBundlePath) {
1001
1182
  throw new Error('--native-bundle requires renderer: "native" in craft.config.json.');
1002
1183
  }
1003
- if (existsSync(join2(output, "app/src/main/jniLibs"))) {
1184
+ if (existsSync(join3(output, "app/src/main/jniLibs"))) {
1004
1185
  const runtimeDir = resolveRuntimeDir(options.runtimeDir);
1005
1186
  if (runtimeDir) {
1006
1187
  installRuntime(output, runtimeDir);
@@ -1028,7 +1209,7 @@ async function build(options) {
1028
1209
  syncNativeFonts(config.nativeFonts, output);
1029
1210
  if (!compile)
1030
1211
  return;
1031
- const gradlewPath = join2(output, "gradlew");
1212
+ const gradlewPath = join3(output, "gradlew");
1032
1213
  if (!existsSync(gradlewPath)) {
1033
1214
  console.log(" Generating Gradle wrapper...");
1034
1215
  try {
@@ -1075,13 +1256,13 @@ async function run(options) {
1075
1256
  await build({ output });
1076
1257
  console.log("\uD83D\uDCF1 Installing and running on device...");
1077
1258
  try {
1078
- const apkPath = join2(output, "app/build/outputs/apk/debug/app-debug.apk");
1259
+ const apkPath = join3(output, "app/build/outputs/apk/debug/app-debug.apk");
1079
1260
  if (device) {
1080
1261
  await $`adb -s ${device} install -r ${apkPath}`;
1081
1262
  } else {
1082
1263
  await $`adb install -r ${apkPath}`;
1083
1264
  }
1084
- const config = JSON.parse(readFileSync2(join2(output, "craft.config.json"), "utf-8"));
1265
+ const config = JSON.parse(readFileSync2(join3(output, "craft.config.json"), "utf-8"));
1085
1266
  const launchCmd = `${config.packageName}/.MainActivity`;
1086
1267
  if (device) {
1087
1268
  await $`adb -s ${device} shell am start -n ${launchCmd}`;
@@ -1108,6 +1289,7 @@ export {
1108
1289
  installRuntime,
1109
1290
  init,
1110
1291
  healthConnectPermissions,
1292
+ createAndroidEmulator,
1111
1293
  build,
1112
1294
  HEALTH_CONNECT_PERMISSIONS,
1113
1295
  ANDROID_MIN_SDK