craft-native 0.0.104 → 0.0.107

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -20,6 +20,7 @@ var __require = import.meta.require;
20
20
  // src/package.ts
21
21
  var exports_package = {};
22
22
  __export(exports_package, {
23
+ windowsNotificationAppId: () => windowsNotificationAppId,
23
24
  windowsExecutableName: () => windowsExecutableName,
24
25
  windowsArchitecture: () => windowsArchitecture,
25
26
  urlTypesEntry: () => urlTypesEntry,
@@ -32,11 +33,14 @@ __export(exports_package, {
32
33
  pack: () => pack,
33
34
  notarytoolArguments: () => notarytoolArguments,
34
35
  macOSInfoPlist: () => macOSInfoPlist,
36
+ linuxDesktopEntry: () => linuxDesktopEntry,
35
37
  formatPackagingCommandError: () => formatPackagingCommandError,
36
38
  dmgCreateArguments: () => dmgCreateArguments,
37
39
  dmgCapacityMegabytes: () => dmgCapacityMegabytes,
38
40
  codesignArguments: () => codesignArguments,
39
- candleArguments: () => candleArguments
41
+ candleArguments: () => candleArguments,
42
+ WINDOWS_NOTIFICATION_ID_FILE: () => WINDOWS_NOTIFICATION_ID_FILE,
43
+ DEFAULT_DEB_DEPENDENCIES: () => DEFAULT_DEB_DEPENDENCIES
40
44
  });
41
45
  import { spawn } from "child_process";
42
46
  import { createHash } from "crypto";
@@ -277,6 +281,9 @@ async function packageWindows(config, outDir) {
277
281
  outputPath: join(outDir, `${name}-${version2}.msi`),
278
282
  manufacturer: config.author || "Unknown",
279
283
  architecture: opts.architecture || windowsArchitecture(process.arch),
284
+ additionalFiles: opts.additionalFiles,
285
+ urlSchemes: opts.urlSchemes,
286
+ launchUrl: opts.launchUrl,
280
287
  certificatePath: opts.certificatePath,
281
288
  certificatePassword: opts.certificatePassword
282
289
  });
@@ -292,7 +299,9 @@ async function packageWindows(config, outDir) {
292
299
  const zipResult = await createZIP({
293
300
  name,
294
301
  version: version2,
302
+ manufacturer: config.author || "Unknown",
295
303
  binaryPath: config.binaryPath,
304
+ additionalFiles: opts.additionalFiles,
296
305
  outputPath: join(outDir, `${name}-${version2}-windows.zip`)
297
306
  });
298
307
  results.push({
@@ -317,7 +326,9 @@ async function packageLinux(config, outDir) {
317
326
  outputPath: join(outDir, `${name}_${version2}_amd64.deb`),
318
327
  description: config.description || "",
319
328
  maintainer: config.author || "Unknown",
320
- dependencies: opts.debDependencies || ["libgtk-3-0", "libwebkit2gtk-4.1-37"]
329
+ dependencies: opts.debDependencies || [...DEFAULT_DEB_DEPENDENCIES],
330
+ urlSchemes: opts.urlSchemes,
331
+ launchUrl: opts.launchUrl
321
332
  });
322
333
  results.push({
323
334
  success: debResult.success,
@@ -363,17 +374,7 @@ async function packageLinux(config, outDir) {
363
374
  return results;
364
375
  }
365
376
  function urlTypesEntry(bundleId, schemes) {
366
- const seen = new Set;
367
- const unique = [];
368
- for (const scheme of schemes) {
369
- if (!URL_SCHEME.test(scheme))
370
- throw new Error(`Invalid URL scheme ${JSON.stringify(scheme)}: must start with a letter and contain only letters, digits, "+", "-" or "."`);
371
- const key = scheme.toLowerCase();
372
- if (seen.has(key))
373
- continue;
374
- seen.add(key);
375
- unique.push(scheme);
376
- }
377
+ const unique = uniqueUrlSchemes(schemes);
377
378
  if (unique.length === 0)
378
379
  return null;
379
380
  const list = unique.map((scheme) => ` <string>${xml(scheme)}</string>`).join(`
@@ -392,6 +393,46 @@ ${list}
392
393
  </dict>
393
394
  </array>`;
394
395
  }
396
+ function uniqueUrlSchemes(schemes) {
397
+ const seen = new Set;
398
+ const unique = [];
399
+ for (const scheme of schemes) {
400
+ if (!URL_SCHEME.test(scheme))
401
+ throw new Error(`Invalid URL scheme ${JSON.stringify(scheme)}: must start with a letter and contain only letters, digits, "+", "-" or "."`);
402
+ const key = scheme.toLowerCase();
403
+ if (seen.has(key))
404
+ continue;
405
+ seen.add(key);
406
+ unique.push(scheme);
407
+ }
408
+ return unique;
409
+ }
410
+ function launchPageUrl(value) {
411
+ let url;
412
+ try {
413
+ url = new URL(value);
414
+ } catch {
415
+ throw new Error(`Invalid packaged app launch URL: ${JSON.stringify(value)}`);
416
+ }
417
+ if (!["http:", "https:", "file:"].includes(url.protocol))
418
+ throw new Error(`Unsupported packaged app launch URL protocol: ${url.protocol}`);
419
+ return url.href;
420
+ }
421
+ function desktopExecArgument(value) {
422
+ return `"${value.replaceAll("%", "%%").replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("$", "\\$").replaceAll("`", "\\`")}"`;
423
+ }
424
+ function linuxDesktopEntry(name, binaryName, urlSchemes = [], launchUrl) {
425
+ const schemes = uniqueUrlSchemes(urlSchemes);
426
+ const page = launchUrl ? ` --url ${desktopExecArgument(launchPageUrl(launchUrl))}` : "";
427
+ return `[Desktop Entry]
428
+ Type=Application
429
+ Name=${name}
430
+ Exec=/usr/bin/${binaryName}${page}${schemes.length ? " --deep-link %u" : ""}
431
+ Terminal=false
432
+ Categories=Utility;
433
+ ${schemes.length ? `MimeType=${schemes.map((scheme) => `x-scheme-handler/${scheme.toLowerCase()};`).join("")}
434
+ ` : ""}`;
435
+ }
395
436
  function macOSInfoPlist(metadata) {
396
437
  const entries = [
397
438
  ["CFBundleExecutable", metadata.name],
@@ -429,7 +470,7 @@ function codesignArguments(opts) {
429
470
  "--force",
430
471
  "--sign",
431
472
  opts.identity,
432
- "--timestamp",
473
+ ...opts.identity === "-" ? [] : ["--timestamp"],
433
474
  ...opts.hardenedRuntime ? ["--options", "runtime"] : [],
434
475
  ...opts.entitlements ? ["--entitlements", opts.entitlements] : [],
435
476
  "--deep",
@@ -654,6 +695,9 @@ function deterministicGuid(value) {
654
695
  const hex = digest.join("").toUpperCase();
655
696
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
656
697
  }
698
+ function windowsNotificationAppId(name, manufacturer) {
699
+ return `Craft.${deterministicGuid(`${manufacturer.trim() || "Unknown"}/${name}`)}`;
700
+ }
657
701
  function windowsArchitecture(architecture) {
658
702
  if (architecture === "ia32" || architecture === "x86")
659
703
  return "x86";
@@ -661,7 +705,7 @@ function windowsArchitecture(architecture) {
661
705
  return architecture;
662
706
  throw new Error(`Unsupported Windows package architecture: ${architecture}`);
663
707
  }
664
- function renderWixSource(opts, sourceName) {
708
+ function renderWixSource(opts, sourceName, additionalFileNames = []) {
665
709
  if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:\.[0-9]+)?$/.test(opts.version))
666
710
  throw new Error(`MSI version must have 3 or 4 numeric parts: ${opts.version}`);
667
711
  const id = wixIdentifier(opts.name);
@@ -669,6 +713,9 @@ function renderWixSource(opts, sourceName) {
669
713
  const upgradeCode = deterministicGuid(`${manufacturer}/${opts.name}`);
670
714
  const programFilesFolder = opts.architecture === "x86" ? "ProgramFilesFolder" : "ProgramFiles64Folder";
671
715
  const win64 = opts.architecture === "x86" ? "no" : "yes";
716
+ const notificationAppId = windowsNotificationAppId(opts.name, manufacturer);
717
+ const schemes = uniqueUrlSchemes(opts.urlSchemes || []);
718
+ const page = opts.launchUrl ? ` --url &quot;${xml(launchPageUrl(opts.launchUrl))}&quot;` : "";
672
719
  return `<?xml version="1.0" encoding="UTF-8"?>
673
720
  <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
674
721
  <Product Id="*" Name="${xml(opts.name)}" Language="1033" Version="${opts.version}" Manufacturer="${xml(manufacturer)}" UpgradeCode="${upgradeCode}">
@@ -679,18 +726,59 @@ function renderWixSource(opts, sourceName) {
679
726
  <Directory Id="${programFilesFolder}">
680
727
  <Directory Id="INSTALLFOLDER" Name="${xml(opts.name)}">
681
728
  <Component Id="${id}Executable" Guid="*" Win64="${win64}">
682
- <File Id="${id}File" Source="${xml(sourceName)}" KeyPath="yes" />
729
+ <File Id="${id}File" Source="${xml(sourceName)}" KeyPath="yes">
730
+ <Shortcut Id="${id}StartMenuShortcut" Directory="ProgramMenuFolder" Name="${xml(opts.name)}" WorkingDirectory="INSTALLFOLDER"${page ? ` Arguments="${page.trim()}"` : ""}>
731
+ <ShortcutProperty Key="System.AppUserModel.ID" Value="${notificationAppId}" />
732
+ </Shortcut>
733
+ </File>
734
+ </Component>
735
+ <Component Id="${id}NotificationIdentity" Guid="*" Win64="${win64}">
736
+ <File Id="${id}NotificationIdentityFile" Source="${WINDOWS_NOTIFICATION_ID_FILE}" KeyPath="yes" />
683
737
  </Component>
738
+ ${additionalFileNames.map((name, index) => ` <Component Id="${id}Extra${index}" Guid="*" Win64="${win64}">
739
+ <File Id="${id}ExtraFile${index}" Source="${xml(name)}" KeyPath="yes" />
740
+ </Component>`).join(`
741
+ `)}
742
+ ${schemes.map((scheme, index) => ` <Component Id="${id}Protocol${index}" Guid="${deterministicGuid(`${manufacturer}/${opts.name}/protocol/${scheme.toLowerCase()}`)}" Win64="${win64}">
743
+ <RegistryKey Root="HKLM" Key="Software\\Classes\\${xml(scheme.toLowerCase())}" Action="createAndRemoveOnUninstall">
744
+ <RegistryValue Type="string" Value="URL:${xml(opts.name)} Protocol" KeyPath="yes" />
745
+ <RegistryValue Type="string" Name="URL Protocol" Value="" />
746
+ <RegistryKey Key="shell\\open\\command">
747
+ <RegistryValue Type="string" Value="&quot;[#${id}File]&quot;${page} --deep-link &quot;%1&quot;" />
748
+ </RegistryKey>
749
+ </RegistryKey>
750
+ </Component>`).join(`
751
+ `)}
684
752
  </Directory>
685
753
  </Directory>
754
+ <Directory Id="ProgramMenuFolder" />
686
755
  </Directory>
687
756
  <Feature Id="ProductFeature" Title="${xml(opts.name)}" Level="1">
688
757
  <ComponentRef Id="${id}Executable" />
758
+ <ComponentRef Id="${id}NotificationIdentity" />
759
+ ${additionalFileNames.map((_, index) => ` <ComponentRef Id="${id}Extra${index}" />`).join(`
760
+ `)}
761
+ ${schemes.map((_, index) => ` <ComponentRef Id="${id}Protocol${index}" />`).join(`
762
+ `)}
689
763
  </Feature>
690
764
  </Product>
691
765
  </Wix>
692
766
  `;
693
767
  }
768
+ function windowsCompanionFiles(paths, executableName) {
769
+ const seen = new Set([executableName.toLowerCase(), WINDOWS_NOTIFICATION_ID_FILE.toLowerCase()]);
770
+ return (paths || []).map((path2) => {
771
+ const name = basename(path2);
772
+ if (!name || /[<>:"/\\|?*\u0000-\u001F]/.test(name) || /[. ]$/.test(name) || /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i.test(name))
773
+ throw new Error(`Invalid Windows companion file name: ${JSON.stringify(name)}`);
774
+ if (seen.has(name.toLowerCase()))
775
+ throw new Error(`Duplicate Windows package file: ${name}`);
776
+ seen.add(name.toLowerCase());
777
+ if (!existsSync3(path2) || lstatSync(path2).isDirectory())
778
+ throw new Error(`Windows companion file not found: ${path2}`);
779
+ return { path: path2, name };
780
+ });
781
+ }
694
782
  function windowsExecutableName(name) {
695
783
  if (name.length === 0 || name.trim() !== name || /[<>:"/\\|?*\u0000-\u001F]/.test(name) || /[. ]$/.test(name))
696
784
  throw new Error(`Invalid Windows application name: ${JSON.stringify(name)}`);
@@ -712,11 +800,15 @@ async function createMSI(opts) {
712
800
  const tempDir = mkdtempSync(join(tmpdir(), "craft-msi-"));
713
801
  try {
714
802
  const binaryName = windowsExecutableName(opts.name);
803
+ const additionalFiles = windowsCompanionFiles(opts.additionalFiles, binaryName);
715
804
  const sourcePath = join(tempDir, binaryName);
716
805
  const wxsPath = join(tempDir, "installer.wxs");
717
806
  const wixobjPath = join(tempDir, "installer.wixobj");
718
807
  copyFileSync(opts.binaryPath, sourcePath);
719
- writeFileSync2(wxsPath, renderWixSource(opts, binaryName));
808
+ writeFileSync2(join(tempDir, WINDOWS_NOTIFICATION_ID_FILE), windowsNotificationAppId(opts.name, opts.manufacturer));
809
+ for (const file of additionalFiles)
810
+ copyFileSync(file.path, join(tempDir, file.name));
811
+ writeFileSync2(wxsPath, renderWixSource(opts, binaryName, additionalFiles.map((file) => file.name)));
720
812
  await runCommand("candle.exe", candleArguments(opts.architecture, wixobjPath, wxsPath), tempDir);
721
813
  await runCommand("light.exe", ["-nologo", "-sval", "-out", opts.outputPath, wixobjPath], tempDir);
722
814
  if (opts.certificatePath) {
@@ -738,8 +830,13 @@ async function createMSI(opts) {
738
830
  }
739
831
  async function createZIP(opts) {
740
832
  try {
741
- const data = new Uint8Array(readFileSync2(opts.binaryPath));
742
- const zip = buildZip([{ name: `${opts.name}.exe`, data }]);
833
+ const binaryName = windowsExecutableName(opts.name);
834
+ const additionalFiles = windowsCompanionFiles(opts.additionalFiles, binaryName);
835
+ const entries = [{ name: binaryName, data: new Uint8Array(readFileSync2(opts.binaryPath)) }];
836
+ entries.push({ name: WINDOWS_NOTIFICATION_ID_FILE, data: new TextEncoder().encode(windowsNotificationAppId(opts.name, opts.manufacturer)) });
837
+ for (const file of additionalFiles)
838
+ entries.push({ name: file.name, data: new Uint8Array(readFileSync2(file.path)) });
839
+ const zip = buildZip(entries);
743
840
  writeFileSync2(opts.outputPath, zip);
744
841
  return { success: true, outputPath: opts.outputPath };
745
842
  } catch (err) {
@@ -769,6 +866,7 @@ async function createDEB(opts) {
769
866
  return;
770
867
  }
771
868
  }
869
+ const desktopContent = linuxDesktopEntry(opts.name, sanitizedName, opts.urlSchemes, opts.launchUrl);
772
870
  const tempDir = mkdtempSync(join(tmpdir(), "craft-deb-"));
773
871
  const debianDir = join(tempDir, "DEBIAN");
774
872
  const binDir = join(tempDir, "usr", "bin");
@@ -789,13 +887,6 @@ Maintainer: ${maintainer}
789
887
  Description: ${description || opts.name}
790
888
  `;
791
889
  writeFileSync2(join(debianDir, "control"), controlContent);
792
- const desktopContent = `[Desktop Entry]
793
- Type=Application
794
- Name=${opts.name}
795
- Exec=/usr/bin/${binaryName}
796
- Terminal=false
797
- Categories=Utility;
798
- `;
799
890
  writeFileSync2(join(applicationsDir, `${binaryName}.desktop`), desktopContent);
800
891
  const proc = spawn("dpkg-deb", ["--build", tempDir, opts.outputPath]);
801
892
  proc.on("close", (code) => {
@@ -1031,8 +1122,9 @@ async function pack(options) {
1031
1122
  platforms: [detectPlatform()]
1032
1123
  });
1033
1124
  }
1034
- var CRC32_TABLE, URL_SCHEME, MEBIBYTE, HDIUTIL_MAX_ATTEMPTS = 3, runHdiutil = (args) => runTool("hdiutil", args);
1125
+ var DEFAULT_DEB_DEPENDENCIES, CRC32_TABLE, URL_SCHEME, MEBIBYTE, HDIUTIL_MAX_ATTEMPTS = 3, runHdiutil = (args) => runTool("hdiutil", args), WINDOWS_NOTIFICATION_ID_FILE = "craft-notification-id.txt";
1035
1126
  var init_package = __esm(() => {
1127
+ DEFAULT_DEB_DEPENDENCIES = ["libgtk-3-0", "libwebkit2gtk-4.1-37", "libnotify-bin"];
1036
1128
  CRC32_TABLE = (() => {
1037
1129
  const table = new Uint32Array(256);
1038
1130
  for (let n = 0;n < 256; n++) {
@@ -1047,1416 +1139,6 @@ var init_package = __esm(() => {
1047
1139
  MEBIBYTE = 1024 * 1024;
1048
1140
  });
1049
1141
 
1050
- // dist/ios/src/index.js
1051
- var exports_src = {};
1052
- __export(exports_src, {
1053
- syncWebAssets: () => syncWebAssets,
1054
- showSimulator: () => showSimulator,
1055
- run: () => run,
1056
- resolveRuntimeDir: () => resolveRuntimeDir,
1057
- resolveAppearance: () => resolveAppearance,
1058
- renderWatchEntitlements: () => renderWatchEntitlements,
1059
- renderUsageDescriptions: () => renderUsageDescriptions,
1060
- renderUrlTypes: () => renderUrlTypes,
1061
- renderRuntimeSettings: () => renderRuntimeSettings,
1062
- renderPrivacyManifest: () => renderPrivacyManifest,
1063
- renderOrientations: () => renderOrientations,
1064
- renderEntitlements: () => renderEntitlements,
1065
- renderDeviceFamilies: () => renderDeviceFamilies,
1066
- renderBackgroundModes: () => renderBackgroundModes,
1067
- renderAppearance: () => renderAppearance,
1068
- productName: () => productName,
1069
- pickSimulator: () => pickSimulator,
1070
- orderSimulators: () => orderSimulators,
1071
- open: () => open,
1072
- installRuntime: () => installRuntime,
1073
- init: () => init,
1074
- build: () => build,
1075
- bootSimulator: () => bootSimulator
1076
- });
1077
- import { cpSync as cpSync2, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync3 } from "fs";
1078
- import { dirname, join as join2, resolve } from "path";
1079
- function renderDeviceFamilies(config) {
1080
- const values = new Set(((config.deviceFamilies?.length) ? config.deviceFamilies : ["iphone", "ipad"]).map((family) => family === "ipad" ? "2" : "1"));
1081
- return [...values].join(",");
1082
- }
1083
- function xmlEscape(value) {
1084
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll(/\u0027/g, "&apos;");
1085
- }
1086
- function plistString(key, value) {
1087
- return ` <key>${key}</key>
1088
- <string>${xmlEscape(value)}</string>`;
1089
- }
1090
- function resolveAppearance(config) {
1091
- if (config.appearance)
1092
- return config.appearance;
1093
- return config.darkMode === false ? "light" : "dark";
1094
- }
1095
- function renderAppearance(config) {
1096
- const appearance = resolveAppearance(config);
1097
- if (appearance === "system")
1098
- return { interfaceStyle: "Automatic", statusBarStyle: "UIStatusBarStyleDefault" };
1099
- if (appearance === "light")
1100
- return { interfaceStyle: "Light", statusBarStyle: "UIStatusBarStyleDarkContent" };
1101
- return { interfaceStyle: "Dark", statusBarStyle: "UIStatusBarStyleLightContent" };
1102
- }
1103
- function renderUsageDescriptions(config) {
1104
- const entries = [
1105
- [config.enableSpeechRecognition, "NSSpeechRecognitionUsageDescription", `${config.appName} uses speech recognition for voice input.`],
1106
- [config.enableSpeechRecognition || config.enableAudioRecording, "NSMicrophoneUsageDescription", `${config.appName} uses the microphone to record audio.`],
1107
- [config.enableCamera || config.enableVideoRecording || config.enableQRScanner || config.enableAR, "NSCameraUsageDescription", `${config.appName} uses the camera for photos, video, scanning, and augmented reality.`],
1108
- [config.enableCamera || config.enableVideoRecording, "NSPhotoLibraryUsageDescription", `${config.appName} lets you choose photos and videos from your library.`],
1109
- [config.enableGeolocation || config.enableBackgroundLocation, "NSLocationWhenInUseUsageDescription", `${config.appName} uses your location while you record an activity.`],
1110
- [config.enableBackgroundLocation, "NSLocationAlwaysAndWhenInUseUsageDescription", `${config.appName} continues recording your route when the screen is locked or the app is in the background.`],
1111
- [config.enableContacts, "NSContactsUsageDescription", `${config.appName} accesses contacts only when you choose a contact feature.`],
1112
- [config.enableCalendar, "NSCalendarsUsageDescription", `${config.appName} accesses your calendar only when you choose a calendar feature.`],
1113
- [config.enableBluetooth, "NSBluetoothAlwaysUsageDescription", `${config.appName} uses Bluetooth to connect to nearby devices.`],
1114
- [config.enableMotionSensors, "NSMotionUsageDescription", `${config.appName} uses motion data for activity features.`],
1115
- [config.enableNFC, "NFCReaderUsageDescription", `${config.appName} reads NFC tags when you start a scan.`],
1116
- [config.enableHealthKit, "NSHealthShareUsageDescription", `${config.appName} reads health data you choose to share.`],
1117
- [config.enableHealthKit, "NSHealthUpdateUsageDescription", `${config.appName} writes health data only with your permission.`],
1118
- [config.enableBiometric, "NSFaceIDUsageDescription", `${config.appName} uses Face ID to protect your account.`]
1119
- ];
1120
- return entries.filter(([enabled2]) => enabled2).map(([, key, value]) => plistString(key, value)).join(`
1121
- `);
1122
- }
1123
- function renderOrientations(config) {
1124
- const names = {
1125
- portrait: "UIInterfaceOrientationPortrait",
1126
- "landscape-left": "UIInterfaceOrientationLandscapeLeft",
1127
- "landscape-right": "UIInterfaceOrientationLandscapeRight",
1128
- "portrait-upside-down": "UIInterfaceOrientationPortraitUpsideDown"
1129
- };
1130
- const values = config.orientations?.length ? config.orientations : ["portrait"];
1131
- return values.map((value) => ` <string>${names[value]}</string>`).join(`
1132
- `);
1133
- }
1134
- function renderUrlTypes(config) {
1135
- const schemes = [...new Set(config.urlSchemes?.map((value) => value.trim()).filter(Boolean) ?? [])];
1136
- if (!schemes.length)
1137
- return "";
1138
- return ` <key>CFBundleURLTypes</key>
1139
- <array>
1140
- <dict>
1141
- <key>CFBundleURLSchemes</key>
1142
- <array>
1143
- ${schemes.map((value) => ` <string>${xmlEscape(value)}</string>`).join(`
1144
- `)}
1145
- </array>
1146
- </dict>
1147
- </array>`;
1148
- }
1149
- function plistArray(values, indent = 2) {
1150
- const padding = " ".repeat(indent);
1151
- return values.map((value) => `${padding}<string>${xmlEscape(value)}</string>`).join(`
1152
- `);
1153
- }
1154
- function renderBackgroundModes(config) {
1155
- const modes = new Set;
1156
- if (config.enableBackgroundLocation)
1157
- modes.add("location");
1158
- if (config.enableBackgroundTasks)
1159
- modes.add("processing");
1160
- if (config.enablePushNotifications)
1161
- modes.add("remote-notification");
1162
- if (!modes.size)
1163
- return "";
1164
- return ` <key>UIBackgroundModes</key>
1165
- <array>
1166
- ${plistArray([...modes])}
1167
- </array>`;
1168
- }
1169
- function renderEntitlements(config) {
1170
- const entries = [];
1171
- if (config.associatedDomains?.length) {
1172
- entries.push(` <key>com.apple.developer.associated-domains</key>
1173
- <array>
1174
- ${plistArray(config.associatedDomains)}
1175
- </array>`);
1176
- }
1177
- if (config.appGroups?.length) {
1178
- entries.push(` <key>com.apple.security.application-groups</key>
1179
- <array>
1180
- ${plistArray(config.appGroups)}
1181
- </array>`);
1182
- }
1183
- if (config.enableHealthKit) {
1184
- entries.push(` <key>com.apple.developer.healthkit</key>
1185
- <true/>`);
1186
- }
1187
- if (config.enablePushNotifications) {
1188
- entries.push(` <key>aps-environment</key>
1189
- <string>$(CRAFT_APNS_ENVIRONMENT)</string>`);
1190
- }
1191
- return `<?xml version="1.0" encoding="UTF-8"?>
1192
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1193
- <plist version="1.0">
1194
- <dict>
1195
- ${entries.join(`
1196
- `)}
1197
- </dict>
1198
- </plist>
1199
- `;
1200
- }
1201
- function renderWatchEntitlements(config) {
1202
- const appGroups = config.appGroups?.length ? ` <key>com.apple.security.application-groups</key>
1203
- <array>
1204
- ${plistArray(config.appGroups)}
1205
- </array>
1206
- ` : "";
1207
- return `<?xml version="1.0" encoding="UTF-8"?>
1208
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1209
- <plist version="1.0">
1210
- <dict>
1211
- ${appGroups}</dict>
1212
- </plist>
1213
- `;
1214
- }
1215
- function renderRuntimeSettings() {
1216
- return [
1217
- ' LIBRARY_SEARCH_PATHS[sdk=iphoneos*]: "$(PROJECT_DIR)/Runtime/device"',
1218
- ' LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]: "$(PROJECT_DIR)/Runtime/simulator"',
1219
- ' OTHER_LDFLAGS: "-lcraft-ios -Wl,-u,_craft_ios_handle_action -Wl,-u,_craft_ios_set_webview ' + '-Wl,-u,_craft_ios_deliver_result -Wl,-u,_craft_ios_deliver_error"'
1220
- ].join(`
1221
- `);
1222
- }
1223
- function renderPrivacyManifest(config) {
1224
- const privacy = config.privacy ?? {};
1225
- const collected = privacy.collectedDataTypes ?? [];
1226
- const accessed = privacy.accessedApiTypes ?? [];
1227
- const collectedXml = collected.map((item) => ` <dict>
1228
- <key>NSPrivacyCollectedDataType</key>
1229
- <string>${xmlEscape(item.type)}</string>
1230
- <key>NSPrivacyCollectedDataTypeLinked</key>
1231
- <${item.linked ? "true" : "false"}/>
1232
- <key>NSPrivacyCollectedDataTypeTracking</key>
1233
- <${item.tracking ? "true" : "false"}/>
1234
- <key>NSPrivacyCollectedDataTypePurposes</key>
1235
- <array>
1236
- ${plistArray(item.purposes, 4)}
1237
- </array>
1238
- </dict>`).join(`
1239
- `);
1240
- const accessedXml = accessed.map((item) => ` <dict>
1241
- <key>NSPrivacyAccessedAPIType</key>
1242
- <string>${xmlEscape(item.type)}</string>
1243
- <key>NSPrivacyAccessedAPITypeReasons</key>
1244
- <array>
1245
- ${plistArray(item.reasons, 4)}
1246
- </array>
1247
- </dict>`).join(`
1248
- `);
1249
- return `<?xml version="1.0" encoding="UTF-8"?>
1250
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1251
- <plist version="1.0">
1252
- <dict>
1253
- <key>NSPrivacyTracking</key>
1254
- <${privacy.tracking ? "true" : "false"}/>
1255
- <key>NSPrivacyTrackingDomains</key>
1256
- <array>
1257
- ${plistArray(privacy.trackingDomains ?? [])}
1258
- </array>
1259
- <key>NSPrivacyCollectedDataTypes</key>
1260
- <array>
1261
- ${collectedXml}
1262
- </array>
1263
- <key>NSPrivacyAccessedAPITypes</key>
1264
- <array>
1265
- ${accessedXml}
1266
- </array>
1267
- </dict>
1268
- </plist>
1269
- `;
1270
- }
1271
- function renderAssetCatalog(output, config) {
1272
- const catalog = join2(output, "Assets.xcassets");
1273
- const appIcon = join2(catalog, "AppIcon.appiconset");
1274
- const launchBackground = join2(catalog, "LaunchBackground.colorset");
1275
- mkdirSync2(appIcon, { recursive: true });
1276
- mkdirSync2(launchBackground, { recursive: true });
1277
- writeFileSync3(join2(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
1278
- `);
1279
- const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
1280
- if (config.appIconPath) {
1281
- if (!existsSync4(config.appIconPath))
1282
- throw new Error(`App icon not found: ${config.appIconPath}`);
1283
- cpSync2(config.appIconPath, join2(appIcon, iconFilename));
1284
- }
1285
- writeFileSync3(join2(appIcon, "Contents.json"), `${JSON.stringify({
1286
- images: iconFilename ? [{
1287
- filename: iconFilename,
1288
- idiom: "universal",
1289
- platform: "ios",
1290
- size: "1024x1024"
1291
- }] : [],
1292
- info: { author: "xcode", version: 1 }
1293
- }, null, 2)}
1294
- `);
1295
- const colorEntry = (hex) => {
1296
- const color = hex?.replace(/^#/, "") || "000000";
1297
- const normalized = color.length === 3 ? [...color].map((value) => `${value}${value}`).join("") : color.padEnd(6, "0").slice(0, 6);
1298
- const components = [0, 2, 4].map((index) => (Number.parseInt(normalized.slice(index, index + 2), 16) / 255).toFixed(3));
1299
- return {
1300
- "color-space": "srgb",
1301
- components: { alpha: "1.000", blue: components[2], green: components[1], red: components[0] }
1302
- };
1303
- };
1304
- const colors2 = [{ color: colorEntry(config.backgroundColor), idiom: "universal" }];
1305
- if (config.backgroundColorDark) {
1306
- colors2.push({
1307
- appearances: [{ appearance: "luminosity", value: "dark" }],
1308
- color: colorEntry(config.backgroundColorDark),
1309
- idiom: "universal"
1310
- });
1311
- }
1312
- writeFileSync3(join2(launchBackground, "Contents.json"), `${JSON.stringify({
1313
- colors: colors2,
1314
- info: { author: "xcode", version: 1 }
1315
- }, null, 2)}
1316
- `);
1317
- }
1318
- function syncWebAssets(source, output) {
1319
- const sourcePath = resolve(source);
1320
- if (!existsSync4(sourcePath))
1321
- throw new Error(`Web asset path not found: ${source}`);
1322
- const distDir = join2(output, "dist");
1323
- rmSync2(distDir, { recursive: true, force: true });
1324
- mkdirSync2(distDir, { recursive: true });
1325
- if (statSync(sourcePath).isDirectory()) {
1326
- cpSync2(sourcePath, distDir, { recursive: true });
1327
- } else {
1328
- cpSync2(sourcePath, join2(distDir, "index.html"));
1329
- }
1330
- if (!existsSync4(join2(distDir, "index.html"))) {
1331
- throw new Error(`Web asset directory must contain index.html: ${source}`);
1332
- }
1333
- }
1334
- function resolveRuntimeDir(override) {
1335
- if (override === null)
1336
- return null;
1337
- const dir = override ?? process.env.CRAFT_IOS_RUNTIME;
1338
- if (!dir)
1339
- return null;
1340
- if (!existsSync4(dir)) {
1341
- const source = override === undefined ? "CRAFT_IOS_RUNTIME points at" : "runtimeDir is";
1342
- throw new Error(`${source} ${dir}, which does not exist.`);
1343
- }
1344
- return dir;
1345
- }
1346
- async function installRuntime(output, runtimeDir) {
1347
- const resolved = Object.entries(RUNTIME_ARCHIVES).map(([sdk, archives]) => {
1348
- const present = archives.filter((a) => existsSync4(join2(runtimeDir, a)));
1349
- if (present.length === 0) {
1350
- throw new Error(`${runtimeDir} has none of ${archives.join(", ")}. ` + `Run \`zig build build-ios-all\` in packages/zig and point at its zig-out/lib.`);
1351
- }
1352
- return { sdk, archives, present };
1353
- });
1354
- const dest = join2(output, "Runtime");
1355
- rmSync2(dest, { recursive: true, force: true });
1356
- for (const { sdk, archives, present } of resolved) {
1357
- const sdkDir = join2(dest, sdk);
1358
- mkdirSync2(sdkDir, { recursive: true });
1359
- const target = join2(sdkDir, "libcraft-ios.a");
1360
- if (present.length === 1) {
1361
- const missing = archives.filter((a) => !present.includes(a));
1362
- if (missing.length > 0) {
1363
- console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
1364
- }
1365
- cpSync2(join2(runtimeDir, present[0]), target);
1366
- } else {
1367
- await $`lipo -create ${present.map((a) => join2(runtimeDir, a))} -output ${target}`.quiet();
1368
- }
1369
- }
1370
- return true;
1371
- }
1372
- async function refreshRuntime(output, override) {
1373
- if (!existsSync4(join2(output, "Runtime")))
1374
- return;
1375
- const dir = resolveRuntimeDir(override);
1376
- if (!dir) {
1377
- console.log(" Keeping the Zig runtime installed at init (no runtime directory configured)");
1378
- return;
1379
- }
1380
- await installRuntime(output, dir);
1381
- console.log(" Refreshed the Zig runtime from", dir);
1382
- }
1383
- function productName(name) {
1384
- const words = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
1385
- const joined = words.map((word) => word[0].toUpperCase() + word.slice(1)).join("");
1386
- if (!joined)
1387
- return "Craft";
1388
- return /^\d/.test(joined) ? `App${joined}` : joined;
1389
- }
1390
- async function init(options) {
1391
- const { name: displayName, bundleId, teamId, output } = options;
1392
- const name = productName(displayName);
1393
- console.log(`
1394
- \u26A1 Initializing Craft iOS project: ${displayName}`);
1395
- console.log(` Output: ${output}
1396
- `);
1397
- const dirs = [output, join2(output, "Sources"), join2(output, "Shared"), join2(output, "dist")];
1398
- if (options.config?.enableWatchApp)
1399
- dirs.push(join2(output, "WatchApp"), join2(output, "WatchExtension"));
1400
- for (const dir of dirs) {
1401
- if (!existsSync4(dir)) {
1402
- mkdirSync2(dir, { recursive: true });
1403
- }
1404
- }
1405
- const finalBundleId = bundleId || `com.craft.${name.toLowerCase()}`;
1406
- const bundleIdPrefix = finalBundleId.split(".").slice(0, -1).join(".");
1407
- const given = Object.fromEntries(Object.entries(options.config ?? {}).filter(([, value]) => value !== undefined));
1408
- const config = {
1409
- ...DEFAULT_CONFIG,
1410
- appName: displayName,
1411
- bundleId: finalBundleId,
1412
- teamId: teamId || "",
1413
- ...given
1414
- };
1415
- if (config.enableBackgroundLocation)
1416
- config.enableGeolocation = true;
1417
- writeFileSync3(join2(output, "craft.config.json"), JSON.stringify(config, null, 2));
1418
- const swiftTemplate = readFileSync3(join2(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
1419
- const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1420
- writeFileSync3(join2(output, "Sources", `${name}App.swift`), swiftSource);
1421
- const infoPlistTemplate = readFileSync3(join2(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
1422
- const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, renderAppearance(config).interfaceStyle).replace(/\{\{STATUS_BAR_STYLE\}\}/g, renderAppearance(config).statusBarStyle).replace(/\{\{ORIENTATIONS\}\}/g, renderOrientations(config)).replace(/\{\{USAGE_DESCRIPTIONS\}\}/g, renderUsageDescriptions(config)).replace(/\{\{URL_TYPES\}\}/g, renderUrlTypes(config)).replace(/\{\{BACKGROUND_MODES\}\}/g, renderBackgroundModes(config)).replace(/\{\{LIVE_ACTIVITY_SUPPORT\}\}/g, config.enableLiveActivities ? ` <key>NSSupportsLiveActivities</key>
1423
- <true/>
1424
- <key>NSSupportsLiveActivitiesFrequentUpdates</key>
1425
- <true/>` : "");
1426
- writeFileSync3(join2(output, "Info.plist"), infoPlist);
1427
- const projectYmlTemplate = readFileSync3(join2(TEMPLATES_DIR, "project.yml.template"), "utf-8");
1428
- const nativeDependencies = [];
1429
- if (config.enableLiveActivities)
1430
- nativeDependencies.push(` - target: ${name}LiveActivity`);
1431
- if (config.enableWatchApp)
1432
- nativeDependencies.push(` - target: ${name}Watch
1433
- embed: true`);
1434
- const nativeTargets = [];
1435
- if (config.enableLiveActivities) {
1436
- nativeTargets.push(` ${name}LiveActivity:
1437
- type: app-extension
1438
- platform: iOS
1439
- deploymentTarget: "16.2"
1440
- sources:
1441
- - WidgetExtension
1442
- - Shared
1443
- settings:
1444
- INFOPLIST_FILE: WidgetExtension/Info.plist
1445
- PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.liveactivity
1446
- SWIFT_VERSION: "5.0"
1447
- APPLICATION_EXTENSION_API_ONLY: YES
1448
- SKIP_INSTALL: YES`);
1449
- }
1450
- if (config.enableWatchApp) {
1451
- nativeTargets.push(` ${name}Watch:
1452
- type: application.watchapp2
1453
- platform: watchOS
1454
- deploymentTarget: "${config.watchosVersion || "9.0"}"
1455
- sources:
1456
- - WatchApp
1457
- settings:
1458
- INFOPLIST_FILE: WatchApp/Info.plist
1459
- CODE_SIGN_ENTITLEMENTS: WatchApp/Watch.entitlements
1460
- PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.watchkitapp
1461
- SWIFT_VERSION: "5.0"
1462
- SKIP_INSTALL: YES
1463
- dependencies:
1464
- - target: ${name}WatchExtension
1465
- ${name}WatchExtension:
1466
- type: watchkit2-extension
1467
- platform: watchOS
1468
- deploymentTarget: "${config.watchosVersion || "9.0"}"
1469
- sources:
1470
- - WatchExtension
1471
- settings:
1472
- INFOPLIST_FILE: WatchExtension/Info.plist
1473
- CODE_SIGN_ENTITLEMENTS: WatchExtension/Watch.entitlements
1474
- PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.watchkitapp.watchkitextension
1475
- SWIFT_VERSION: "5.0"
1476
- APPLICATION_EXTENSION_API_ONLY: YES
1477
- SKIP_INSTALL: YES`);
1478
- }
1479
- const runtimeDir = resolveRuntimeDir(options.runtimeDir);
1480
- const hasRuntime = runtimeDir ? await installRuntime(output, runtimeDir) : false;
1481
- if (hasRuntime) {
1482
- console.log(" Linked the Zig runtime from", runtimeDir);
1483
- } else {
1484
- rmSync2(join2(output, "Runtime"), { recursive: true, force: true });
1485
- }
1486
- const projectYml = projectYmlTemplate.replace(/\{\{CRAFT_RUNTIME_SETTINGS\}\}/g, hasRuntime ? renderRuntimeSettings() : "").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{BUNDLE_ID_PREFIX\}\}/g, bundleIdPrefix).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{IOS_VERSION\}\}/g, config.iosVersion || "15.0").replace(/\{\{DEVICE_FAMILIES\}\}/g, renderDeviceFamilies(config)).replace(/\{\{TEAM_ID\}\}/g, teamId || "").replace(/\{\{NATIVE_DEPENDENCIES\}\}/g, nativeDependencies.length ? ` dependencies:
1487
- ${nativeDependencies.join(`
1488
- `)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
1489
- `));
1490
- writeFileSync3(join2(output, "project.yml"), projectYml);
1491
- writeFileSync3(join2(output, "Craft.entitlements"), renderEntitlements(config));
1492
- writeFileSync3(join2(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
1493
- renderAssetCatalog(output, config);
1494
- cpSync2(join2(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join2(output, "Shared", "CraftActivityAttributes.swift"));
1495
- if (config.enableLiveActivities) {
1496
- mkdirSync2(join2(output, "WidgetExtension"), { recursive: true });
1497
- const widgetSource = readFileSync3(join2(TEMPLATES_DIR, "CraftLiveActivityWidget.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
1498
- writeFileSync3(join2(output, "WidgetExtension", `${name}LiveActivity.swift`), widgetSource);
1499
- const widgetInfo = readFileSync3(join2(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
1500
- writeFileSync3(join2(output, "WidgetExtension", "Info.plist"), widgetInfo);
1501
- }
1502
- if (config.enableWatchApp) {
1503
- const watchSource = readFileSync3(join2(TEMPLATES_DIR, "CraftWatchApp.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
1504
- writeFileSync3(join2(output, "WatchExtension", `${name}WatchApp.swift`), watchSource);
1505
- const watchInfo = readFileSync3(join2(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1506
- writeFileSync3(join2(output, "WatchApp", "Info.plist"), watchInfo);
1507
- writeFileSync3(join2(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
1508
- const watchExtensionInfo = readFileSync3(join2(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1509
- writeFileSync3(join2(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
1510
- writeFileSync3(join2(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
1511
- }
1512
- const placeholderHtml = `<!DOCTYPE html>
1513
- <html>
1514
- <head>
1515
- <meta charset="UTF-8">
1516
- <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
1517
- <title>${xmlEscape(displayName)}</title>
1518
- <style>
1519
- * { margin: 0; padding: 0; box-sizing: border-box; }
1520
- body {
1521
- font-family: -apple-system, system-ui, sans-serif;
1522
- background: ${config.backgroundColor};
1523
- color: white;
1524
- min-height: 100vh;
1525
- display: flex;
1526
- justify-content: center;
1527
- align-items: center;
1528
- padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
1529
- }
1530
- .container { text-align: center; padding: 2rem; }
1531
- h1 { font-size: 2.5rem; margin-bottom: 1rem; }
1532
- p { opacity: 0.7; }
1533
- .ready { color: #4ade80; font-size: 0.9rem; margin-top: 2rem; }
1534
- </style>
1535
- </head>
1536
- <body>
1537
- <div class="container">
1538
- <h1>\u26A1 ${xmlEscape(displayName)}</h1>
1539
- <p>Built with Craft iOS</p>
1540
- <p class="ready" id="status">Waiting for Craft bridge...</p>
1541
- </div>
1542
- <script>
1543
- window.addEventListener('craftReady', (e) => {
1544
- document.getElementById('status').textContent = \`\u2713 Craft bridge ready (platform: \${e.detail.platform})\`;
1545
- console.log('Craft capabilities:', e.detail.capabilities);
1546
- });
1547
- </script>
1548
- </body>
1549
- </html>`;
1550
- writeFileSync3(join2(output, "dist", "index.html"), placeholderHtml);
1551
- console.log("\u2705 Project initialized");
1552
- console.log("");
1553
- console.log("Next steps:");
1554
- console.log(` 1. cd ${output}`);
1555
- console.log(" 2. Add your web content to dist/index.html");
1556
- console.log(" 3. Run: craft ios build");
1557
- console.log(" 4. Run: craft ios open");
1558
- console.log("");
1559
- }
1560
- async function build(options) {
1561
- const { htmlPath, devServer, output, generateProject = true } = options;
1562
- console.log(`
1563
- \uD83D\uDCE6 Building Craft iOS project...`);
1564
- const configPath = join2(output, "craft.config.json");
1565
- if (!existsSync4(configPath)) {
1566
- throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
1567
- }
1568
- const config = JSON.parse(readFileSync3(configPath, "utf-8"));
1569
- if (devServer) {
1570
- config.devServerURL = devServer;
1571
- const origin = new URL(devServer).origin;
1572
- config.trustedOrigins = [...new Set([...config.trustedOrigins ?? [], origin])];
1573
- writeFileSync3(configPath, JSON.stringify(config, null, 2));
1574
- console.log(` Dev server: ${devServer}`);
1575
- }
1576
- if (htmlPath) {
1577
- syncWebAssets(htmlPath, output);
1578
- console.log(` Synced: ${htmlPath} \u2192 dist/`);
1579
- }
1580
- await refreshRuntime(output, options.runtimeDir);
1581
- if (!generateProject)
1582
- return;
1583
- try {
1584
- const result = await $`which xcodegen`.quiet();
1585
- if (result.exitCode === 0) {
1586
- console.log(" Running xcodegen...");
1587
- await $`cd ${output} && xcodegen generate`.quiet();
1588
- console.log(`\u2705 Xcode project created: ${productName(config.appName)}.xcodeproj`);
1589
- } else {
1590
- throw new Error("xcodegen not found");
1591
- }
1592
- } catch (error) {
1593
- throw new Error("Unable to generate the Xcode project. Install xcodegen with `brew install xcodegen`.", {
1594
- cause: error
1595
- });
1596
- }
1597
- console.log("");
1598
- }
1599
- async function open(options) {
1600
- const { output } = options;
1601
- const files = readdirSync2(output);
1602
- const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
1603
- if (!xcodeproj) {
1604
- throw new Error(`No Xcode project found in ${output}. Run 'craft ios build' first.`);
1605
- }
1606
- const projectPath = join2(output, xcodeproj);
1607
- console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
1608
- await $`open ${projectPath}`;
1609
- }
1610
- function orderSimulators(devices) {
1611
- return devices.filter((device) => device.runtime.startsWith("iOS")).sort((left, right) => {
1612
- const booted = Number(right.state === "Booted") - Number(left.state === "Booted");
1613
- if (booted !== 0)
1614
- return booted;
1615
- const phone = Number(right.name.startsWith("iPhone")) - Number(left.name.startsWith("iPhone"));
1616
- if (phone !== 0)
1617
- return phone;
1618
- return right.runtime.localeCompare(left.runtime);
1619
- });
1620
- }
1621
- async function pickSimulator() {
1622
- const listed = await $`xcrun simctl list devices available --json`.quiet().nothrow();
1623
- if (listed.exitCode !== 0)
1624
- return null;
1625
- let parsed;
1626
- try {
1627
- parsed = JSON.parse(listed.stdout.toString());
1628
- } catch {
1629
- return null;
1630
- }
1631
- const devices = [];
1632
- for (const [runtime, list] of Object.entries(parsed.devices ?? {})) {
1633
- for (const device of list)
1634
- devices.push({ ...device, runtime: runtime.split(".").pop() ?? runtime });
1635
- }
1636
- return orderSimulators(devices)[0] ?? null;
1637
- }
1638
- async function bootSimulator(device) {
1639
- if (device.state !== "Booted")
1640
- await $`xcrun simctl boot ${device.udid}`.quiet().nothrow();
1641
- await $`xcrun simctl bootstatus ${device.udid}`.quiet().nothrow();
1642
- }
1643
- async function showSimulator() {
1644
- const selected = await $`xcode-select -p`.quiet().nothrow();
1645
- const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
1646
- if (developer) {
1647
- const app = join2(developer, "Applications", "Simulator.app");
1648
- if (existsSync4(app)) {
1649
- await $`open ${app}`.quiet().nothrow();
1650
- return;
1651
- }
1652
- }
1653
- await $`open -a Simulator`.quiet().nothrow();
1654
- }
1655
- async function run(options) {
1656
- const { simulator, output } = options;
1657
- await build({ output });
1658
- const files = readdirSync2(output);
1659
- const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
1660
- if (!xcodeproj) {
1661
- throw new Error(`No Xcode project found in ${output}`);
1662
- }
1663
- const projectPath = join2(output, xcodeproj);
1664
- const appName = xcodeproj.replace(".xcodeproj", "");
1665
- const configPath = join2(output, "craft.config.json");
1666
- if (!existsSync4(configPath))
1667
- throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
1668
- const config = JSON.parse(readFileSync3(configPath, "utf-8"));
1669
- if (simulator) {
1670
- console.log("\uD83D\uDCF1 Building and running on simulator...");
1671
- const device = await pickSimulator();
1672
- if (!device) {
1673
- throw new Error("No iOS simulator is available. Install a runtime with " + "`xcodebuild -downloadPlatform iOS`, then try again.");
1674
- }
1675
- console.log(` Device: ${device.name} (${device.runtime})`);
1676
- try {
1677
- await bootSimulator(device);
1678
- const derivedData = join2(output, "build");
1679
- const product = join2(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
1680
- await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
1681
- if (!existsSync4(product)) {
1682
- throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
1683
- }
1684
- await $`xcrun simctl install ${device.udid} ${product}`;
1685
- await showSimulator();
1686
- await $`xcrun simctl launch ${device.udid} ${config.bundleId}`;
1687
- console.log(`\u2705 ${config.appName} is running on ${device.name}`);
1688
- } catch (error) {
1689
- throw new Error(`iOS simulator run failed for ${projectPath}`, { cause: error });
1690
- }
1691
- } else {
1692
- console.log("\uD83D\uDCF1 Opening Xcode for device deployment...");
1693
- await $`open ${projectPath}`;
1694
- console.log("");
1695
- console.log("In Xcode:");
1696
- console.log(" 1. Select your Team in Signing & Capabilities");
1697
- console.log(" 2. Connect your iPhone");
1698
- console.log(" 3. Select your device");
1699
- console.log(" 4. Click Run (\u25B6\uFE0F)");
1700
- }
1701
- }
1702
- var $, TEMPLATES_DIR, DEFAULT_CONFIG, RUNTIME_ARCHIVES;
1703
- var init_src = __esm(() => {
1704
- ({ $ } = globalThis.Bun);
1705
- TEMPLATES_DIR = join2(dirname(import.meta.dir), "templates");
1706
- DEFAULT_CONFIG = {
1707
- version: "1.0.0",
1708
- buildNumber: "1",
1709
- darkMode: true,
1710
- backgroundColor: "#0b1712",
1711
- enableSpeechRecognition: false,
1712
- enableHaptics: false,
1713
- enableShare: false,
1714
- enableCamera: false,
1715
- enableBiometric: false,
1716
- enablePushNotifications: false,
1717
- enableSecureStorage: false,
1718
- enableGeolocation: false,
1719
- enableClipboard: false,
1720
- enableContacts: false,
1721
- enableCalendar: false,
1722
- enableLocalNotifications: false,
1723
- enableInAppPurchase: false,
1724
- enableKeepAwake: false,
1725
- enableOrientationLock: false,
1726
- enableDeepLinks: false,
1727
- enableQRScanner: false,
1728
- enableFilePicker: false,
1729
- enableFileDownload: false,
1730
- enableSocialAuth: false,
1731
- enableAudioRecording: false,
1732
- enableVideoRecording: false,
1733
- enableMotionSensors: false,
1734
- enableLocalDatabase: false,
1735
- enableBluetooth: false,
1736
- enableNFC: false,
1737
- enableHealthKit: false,
1738
- enableLiveActivities: false,
1739
- enableWatchApp: false,
1740
- enableBackgroundLocation: false,
1741
- enableBackgroundTasks: false,
1742
- enableScreenCapture: false,
1743
- enablePDFViewer: false,
1744
- enableAR: false,
1745
- enableMLKit: false,
1746
- iosVersion: "16.0",
1747
- watchosVersion: "9.0",
1748
- teamId: "",
1749
- trustedOrigins: [],
1750
- associatedDomains: [],
1751
- appGroups: [],
1752
- orientations: ["portrait"],
1753
- deviceFamilies: ["iphone", "ipad"]
1754
- };
1755
- RUNTIME_ARCHIVES = {
1756
- device: ["libcraft-ios.a"],
1757
- simulator: ["libcraft-ios-simulator-arm64.a", "libcraft-ios-simulator-x64.a"]
1758
- };
1759
- });
1760
-
1761
- // dist/android/src/index.js
1762
- var exports_src2 = {};
1763
- __export(exports_src2, {
1764
- syncAndroidWebAssets: () => syncAndroidWebAssets,
1765
- run: () => run2,
1766
- resolveRuntimeDir: () => resolveRuntimeDir2,
1767
- renderAndroidPermissions: () => renderAndroidPermissions,
1768
- renderAndroidDeepLinks: () => renderAndroidDeepLinks,
1769
- open: () => open2,
1770
- installRuntime: () => installRuntime2,
1771
- init: () => init2,
1772
- build: () => build2,
1773
- ANDROID_MIN_SDK: () => ANDROID_MIN_SDK
1774
- });
1775
- import { cpSync as cpSync3, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
1776
- import { dirname as dirname2, extname, isAbsolute, join as join3, relative, resolve as resolve2, sep } from "path";
1777
- function renderAndroidPromiseRuntime(indent = "") {
1778
- return ANDROID_PROMISE_RUNTIME.trim().split(`
1779
- `).map((line) => `${indent}${line}`).join(`
1780
- `);
1781
- }
1782
- function escapeXml(value) {
1783
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
1784
- }
1785
- function escapeKotlinString(value) {
1786
- return value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("$", "\\$").replaceAll("\r", "\\r").replaceAll(`
1787
- `, "\\n");
1788
- }
1789
- function generatedPackageSegment(name) {
1790
- const normalized = name.toLowerCase().replace(/[^a-z0-9_]/g, "");
1791
- if (!normalized)
1792
- return "app";
1793
- return /^[a-z_]/.test(normalized) ? normalized : `app${normalized}`;
1794
- }
1795
- function generatedGradleProjectName(name) {
1796
- const invalidCharacters = ["/", "\\", ":", "<", ">", '"', "?", "*", "|"];
1797
- const normalized = invalidCharacters.reduce((value, character) => value.replaceAll(character, "-"), name).trim();
1798
- return normalized || "craft-app";
1799
- }
1800
- function requireRegularFile(path2, label) {
1801
- if (!existsSync5(path2))
1802
- throw new Error(`${label} not found: ${path2}`);
1803
- if (!statSync2(path2).isFile())
1804
- throw new Error(`${label} must be a file: ${path2}`);
1805
- }
1806
- function containsPath(parent, candidate) {
1807
- const relativePath = relative(parent, candidate);
1808
- return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
1809
- }
1810
- function validateGoogleServicesFile(path2, packageName) {
1811
- requireRegularFile(path2, "Google services file");
1812
- let document;
1813
- try {
1814
- document = JSON.parse(readFileSync4(path2, "utf8"));
1815
- } catch (error) {
1816
- throw new Error(`Google services file must contain valid JSON: ${path2}`, { cause: error });
1817
- }
1818
- const matchingClient = document.client?.some((client) => {
1819
- return client.client_info?.android_client_info?.package_name === packageName;
1820
- });
1821
- if (!matchingClient) {
1822
- throw new Error(`Google services file has no client for Android package ${packageName}: ${path2}`);
1823
- }
1824
- }
1825
- function androidWebUrl(value, field) {
1826
- let url;
1827
- try {
1828
- url = new URL(value);
1829
- } catch {
1830
- throw new Error(`Invalid ${field}: ${value}`);
1831
- }
1832
- const localDevelopment = url.protocol === "http:" && LOCAL_DEVELOPMENT_HOSTS.has(url.hostname);
1833
- if (url.protocol !== "https:" && !localDevelopment || url.username || url.password) {
1834
- throw new Error(`${field} must use HTTPS or local HTTP without credentials: ${value}`);
1835
- }
1836
- return url;
1837
- }
1838
- function normalizeAndroidNetworkConfig(config) {
1839
- const schemes = config.urlSchemes?.map((value) => value.trim().toLowerCase()) ?? [];
1840
- if (schemes.some((value) => !/^[a-z][a-z0-9+.-]*$/.test(value))) {
1841
- throw new Error("Android deep-link schemes must be valid URI schemes");
1842
- }
1843
- config.urlSchemes = [...new Set(schemes)];
1844
- if (config.enableDeepLinks && config.urlSchemes.length === 0) {
1845
- throw new Error("Android deep links require at least one URL scheme");
1846
- }
1847
- const trustedOrigins = (config.trustedOrigins ?? []).map((value) => {
1848
- return androidWebUrl(value, "Android trusted origin").origin;
1849
- });
1850
- if (config.devServerURL) {
1851
- const devServer = androidWebUrl(config.devServerURL, "Android dev server URL");
1852
- config.devServerURL = devServer.toString();
1853
- trustedOrigins.push(devServer.origin);
1854
- }
1855
- config.trustedOrigins = [...new Set(trustedOrigins)];
1856
- }
1857
- function validateAndroidConfig(config) {
1858
- if (!config.appName.trim())
1859
- throw new Error("Android app name must not be empty");
1860
- const packageSegments = config.packageName.split(".");
1861
- if (packageSegments.length < 2 || packageSegments.some((segment) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment) || KOTLIN_KEYWORDS.has(segment))) {
1862
- throw new Error(`Invalid Android package name: ${config.packageName}`);
1863
- }
1864
- if (!/^#(?:[\dA-F]{3,4}|[\dA-F]{6}|[\dA-F]{8})$/i.test(config.backgroundColor ?? "")) {
1865
- throw new Error(`Invalid Android background color: ${config.backgroundColor}`);
1866
- }
1867
- for (const [name, value] of [
1868
- ["versionCode", config.versionCode],
1869
- ["minSdk", config.minSdk],
1870
- ["compileSdk", config.compileSdk],
1871
- ["targetSdk", config.targetSdk]
1872
- ]) {
1873
- if (!Number.isInteger(value) || Number(value) < 1) {
1874
- throw new Error(`Android ${name} must be a positive integer`);
1875
- }
1876
- }
1877
- if (Number(config.minSdk) < ANDROID_MIN_SDK) {
1878
- throw new Error(`Android minSdk must be at least ${ANDROID_MIN_SDK} (Android 8.0) for the Craft runtime`);
1879
- }
1880
- if (Number(config.minSdk) > Number(config.targetSdk)) {
1881
- throw new Error("Android minSdk must not exceed targetSdk");
1882
- }
1883
- if (Number(config.targetSdk) > Number(config.compileSdk)) {
1884
- throw new Error("Android targetSdk must not exceed compileSdk");
1885
- }
1886
- }
1887
- function writeAndroidConfig(output, config) {
1888
- writeFileSync4(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
1889
- const runtimeConfig = { ...config };
1890
- delete runtimeConfig.appIconPath;
1891
- delete runtimeConfig.googleServicesFile;
1892
- writeFileSync4(join3(output, "app/src/main/assets/craft.config.json"), JSON.stringify(runtimeConfig, null, 2));
1893
- }
1894
- function syncAndroidWebAssets(source, output) {
1895
- const sourcePath = resolve2(source);
1896
- if (!existsSync5(sourcePath))
1897
- throw new Error(`Web asset path not found: ${source}`);
1898
- const sourceStat = statSync2(sourcePath);
1899
- if (!sourceStat.isDirectory() && !sourceStat.isFile()) {
1900
- throw new Error(`Web asset path must be a file or directory: ${source}`);
1901
- }
1902
- const assetsDir = resolve2(output, "app/src/main/assets");
1903
- if (containsPath(sourcePath, assetsDir) || containsPath(assetsDir, sourcePath)) {
1904
- throw new Error(`Web asset source must not overlap generated asset directory: ${source}`);
1905
- }
1906
- if (sourceStat.isDirectory()) {
1907
- requireRegularFile(join3(sourcePath, "index.html"), "Web asset directory entry point");
1908
- }
1909
- const configPath = join3(assetsDir, "craft.config.json");
1910
- const config = existsSync5(configPath) ? readFileSync4(configPath) : undefined;
1911
- rmSync3(assetsDir, { recursive: true, force: true });
1912
- mkdirSync3(assetsDir, { recursive: true });
1913
- if (sourceStat.isDirectory())
1914
- cpSync3(sourcePath, assetsDir, { recursive: true });
1915
- else
1916
- cpSync3(sourcePath, join3(assetsDir, "index.html"));
1917
- if (!existsSync5(join3(assetsDir, "index.html")))
1918
- throw new Error(`Web asset directory must contain index.html: ${source}`);
1919
- if (config)
1920
- writeFileSync4(configPath, config);
1921
- }
1922
- function renderAndroidPermissions(config) {
1923
- const permissions = new Set(["android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE"]);
1924
- if (config.enableSpeechRecognition)
1925
- permissions.add("android.permission.RECORD_AUDIO");
1926
- if (config.enableCamera)
1927
- permissions.add("android.permission.CAMERA");
1928
- if (config.enableHaptics)
1929
- permissions.add("android.permission.VIBRATE");
1930
- if (config.enableBiometric)
1931
- permissions.add("android.permission.USE_BIOMETRIC");
1932
- if (config.enableGeolocation || config.enableBackgroundLocation) {
1933
- permissions.add("android.permission.ACCESS_FINE_LOCATION");
1934
- permissions.add("android.permission.ACCESS_COARSE_LOCATION");
1935
- }
1936
- if (config.enableBackgroundLocation) {
1937
- permissions.add("android.permission.ACCESS_BACKGROUND_LOCATION");
1938
- permissions.add("android.permission.FOREGROUND_SERVICE");
1939
- permissions.add("android.permission.FOREGROUND_SERVICE_LOCATION");
1940
- }
1941
- if (config.enablePushNotifications)
1942
- permissions.add("android.permission.POST_NOTIFICATIONS");
1943
- if (config.enableKeepAwake)
1944
- permissions.add("android.permission.WAKE_LOCK");
1945
- if (config.enableHealthConnect) {
1946
- permissions.add("android.permission.health.READ_STEPS");
1947
- permissions.add("android.permission.health.READ_HEART_RATE");
1948
- permissions.add("android.permission.health.READ_ACTIVE_CALORIES_BURNED");
1949
- permissions.add("android.permission.health.READ_DISTANCE");
1950
- permissions.add("android.permission.health.READ_EXERCISE");
1951
- permissions.add("android.permission.health.WRITE_EXERCISE");
1952
- permissions.add("android.permission.health.WRITE_ACTIVE_CALORIES_BURNED");
1953
- permissions.add("android.permission.health.WRITE_DISTANCE");
1954
- }
1955
- return [...permissions].map((permission) => ` <uses-permission android:name="${permission}" />`).join(`
1956
- `);
1957
- }
1958
- function renderAndroidDeepLinks(config) {
1959
- if (!config.enableDeepLinks)
1960
- return "";
1961
- const schemes = [...new Set(config.urlSchemes?.map((value) => value.trim()).filter((value) => /^[a-z][a-z0-9+.-]*$/i.test(value)) ?? [])];
1962
- return schemes.map((scheme) => ` <intent-filter>
1963
- <action android:name="android.intent.action.VIEW" />
1964
- <category android:name="android.intent.category.DEFAULT" />
1965
- <category android:name="android.intent.category.BROWSABLE" />
1966
- <data android:scheme="${scheme}" />
1967
- </intent-filter>`).join(`
1968
- `);
1969
- }
1970
- function resolveRuntimeDir2(override) {
1971
- if (override === null)
1972
- return null;
1973
- const dir = override ?? process.env.CRAFT_ANDROID_RUNTIME;
1974
- if (!dir)
1975
- return null;
1976
- if (!existsSync5(dir)) {
1977
- const source = override === undefined ? "CRAFT_ANDROID_RUNTIME points at" : "runtimeDir is";
1978
- throw new Error(`${source} ${dir}, which does not exist.`);
1979
- }
1980
- return dir;
1981
- }
1982
- function installRuntime2(output, runtimeDir) {
1983
- const found = RUNTIME_ABIS.map((abi) => ({ abi, source: join3(runtimeDir, abi, "libcraft.so") })).filter((entry) => existsSync5(entry.source));
1984
- if (found.length === 0) {
1985
- 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.");
1986
- }
1987
- if (found.length < RUNTIME_ABIS.length) {
1988
- const missing = RUNTIME_ABIS.filter((abi) => !found.some((entry) => entry.abi === abi));
1989
- 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.");
1990
- }
1991
- const dest = join3(output, "app/src/main/jniLibs");
1992
- rmSync3(dest, { force: true, recursive: true });
1993
- for (const { abi, source } of found) {
1994
- const abiDir = join3(dest, abi);
1995
- mkdirSync3(abiDir, { recursive: true });
1996
- cpSync3(source, join3(abiDir, "libcraft.so"));
1997
- }
1998
- return true;
1999
- }
2000
- async function init2(options) {
2001
- const { name, packageName, output } = options;
2002
- console.log(`
2003
- \u26A1 Initializing Craft Android project: ${name}`);
2004
- console.log(` Output: ${output}
2005
- `);
2006
- const finalPackageName = packageName || `com.craft.${generatedPackageSegment(name)}`;
2007
- const packagePath = finalPackageName.replace(/\./g, "/");
2008
- const given = Object.fromEntries(Object.entries(options.config ?? {}).filter(([, value]) => value !== undefined));
2009
- const config = {
2010
- ...DEFAULT_CONFIG2,
2011
- ...given,
2012
- appName: name,
2013
- packageName: finalPackageName
2014
- };
2015
- if (config.enableBackgroundLocation)
2016
- config.enableGeolocation = true;
2017
- if (config.enableHealthConnect) {
2018
- config.compileSdk = Math.max(config.compileSdk ?? 36, 36);
2019
- }
2020
- normalizeAndroidNetworkConfig(config);
2021
- validateAndroidConfig(config);
2022
- if (config.enablePushNotifications && !config.googleServicesFile) {
2023
- throw new Error("Android push notifications require a googleServicesFile");
2024
- }
2025
- if (config.googleServicesFile)
2026
- validateGoogleServicesFile(config.googleServicesFile, finalPackageName);
2027
- if (config.appIconPath)
2028
- requireRegularFile(config.appIconPath, "App icon");
2029
- const appIconExtension = config.appIconPath ? extname(config.appIconPath).toLowerCase() : undefined;
2030
- if (config.appIconPath && ![".gif", ".jpg", ".png", ".webp"].includes(appIconExtension ?? "")) {
2031
- throw new Error(`Unsupported Android app icon format: ${appIconExtension || "(none)"}`);
2032
- }
2033
- const dirs = [
2034
- output,
2035
- join3(output, "app/src/main/java", packagePath),
2036
- join3(output, "app/src/main/res/layout"),
2037
- join3(output, "app/src/main/res/values"),
2038
- join3(output, "app/src/main/res/drawable"),
2039
- join3(output, "app/src/main/assets"),
2040
- join3(output, "gradle/wrapper")
2041
- ];
2042
- if (existsSync5(output) && !statSync2(output).isDirectory()) {
2043
- throw new Error(`Android project output must be a directory: ${output}`);
2044
- }
2045
- for (const dir of dirs) {
2046
- if (!existsSync5(dir)) {
2047
- mkdirSync3(dir, { recursive: true });
2048
- }
2049
- }
2050
- writeAndroidConfig(output, config);
2051
- const hasGoogleServices = Boolean(config.googleServicesFile);
2052
- if (config.googleServicesFile) {
2053
- cpSync3(config.googleServicesFile, join3(output, "app/google-services.json"));
2054
- }
2055
- const mainActivityTemplate = readFileSync4(join3(TEMPLATES_DIR2, "MainActivity.kt.template"), "utf-8");
2056
- const mainActivity = mainActivityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
2057
- writeFileSync4(join3(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
2058
- const craftBridgeTemplate = readFileSync4(join3(TEMPLATES_DIR2, "CraftBridge.kt.template"), "utf-8");
2059
- 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 {
2060
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
2061
- && ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
2062
- ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 4204)
2063
- }
2064
- try {
2065
- FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
2066
- val token = if (task.isSuccessful) task.result else null
2067
- val callback = if (token.isNullOrBlank()) "window._craftPushReject" else "window._craftPushResolve"
2068
- val payload = JSONObject.quote(token ?: task.exception?.message ?: "Firebase Cloud Messaging is not configured")
2069
- evaluatePromiseJavascript("$callback && $callback($payload)")
2070
- }
2071
- } catch (error: Exception) {
2072
- val payload = JSONObject.quote(error.message ?: "Firebase Cloud Messaging is not configured")
2073
- evaluatePromiseJavascript("window._craftPushReject && window._craftPushReject($payload)")
2074
- }
2075
- }` : `evaluatePromiseJavascript(
2076
- "window._craftPushReject && window._craftPushReject('Push notifications are disabled')"
2077
- )`);
2078
- writeFileSync4(join3(output, "app/src/main/java", packagePath, "CraftBridge.kt"), craftBridge);
2079
- const craftNative = readFileSync4(join3(TEMPLATES_DIR2, "CraftNative.kt.template"), "utf-8");
2080
- const nativeDir = join3(output, "app/src/main/java/com/craft/runtime");
2081
- mkdirSync3(nativeDir, { recursive: true });
2082
- writeFileSync4(join3(nativeDir, "CraftNative.kt"), craftNative);
2083
- const serviceTemplate = readFileSync4(join3(TEMPLATES_DIR2, "LocationRecordingService.kt.template"), "utf-8");
2084
- writeFileSync4(join3(nativeDir, "LocationRecordingService.kt"), serviceTemplate);
2085
- const healthTemplate = readFileSync4(join3(TEMPLATES_DIR2, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
2086
- writeFileSync4(join3(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
2087
- const manifestTemplate = readFileSync4(join3(TEMPLATES_DIR2, "AndroidManifest.xml.template"), "utf-8");
2088
- 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://") ? "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(/\{\{HEALTH_CONNECT_QUERIES\}\}/g, config.enableHealthConnect ? ` <queries>
2089
- <package android:name="com.google.android.apps.healthdata" />
2090
- </queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
2091
- <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
2092
- </intent-filter>
2093
- <intent-filter>
2094
- <action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
2095
- <category android:name="android.intent.category.HEALTH_PERMISSIONS" />
2096
- </intent-filter>` : "");
2097
- writeFileSync4(join3(output, "app/src/main/AndroidManifest.xml"), manifest);
2098
- const projectGradleTemplate = readFileSync4(join3(TEMPLATES_DIR2, "build.gradle.kts.project.template"), "utf-8");
2099
- writeFileSync4(join3(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
2100
- const appGradleTemplate = readFileSync4(join3(TEMPLATES_DIR2, "build.gradle.kts.app.template"), "utf-8");
2101
- 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(/\{\{HEALTH_CONNECT_DEPENDENCIES\}\}/g, config.enableHealthConnect ? ` implementation("androidx.health.connect:connect-client:1.1.0")
2102
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")` : "");
2103
- writeFileSync4(join3(output, "app/build.gradle.kts"), appGradle);
2104
- const proguardTemplate = readFileSync4(join3(TEMPLATES_DIR2, "proguard-rules.pro.template"), "utf-8");
2105
- writeFileSync4(join3(output, "app/proguard-rules.pro"), proguardTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
2106
- const settingsTemplate = readFileSync4(join3(TEMPLATES_DIR2, "settings.gradle.kts.template"), "utf-8");
2107
- const settings2 = settingsTemplate.replace(/\{\{APP_NAME\}\}/g, escapeKotlinString(generatedGradleProjectName(name)));
2108
- writeFileSync4(join3(output, "settings.gradle.kts"), settings2);
2109
- const gradleProperties = `org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2110
- android.useAndroidX=true
2111
- kotlin.code.style=official
2112
- android.nonTransitiveRClass=true
2113
- `;
2114
- writeFileSync4(join3(output, "gradle.properties"), gradleProperties);
2115
- writeFileSync4(join3(output, "local.properties"), `# SDK location will be set by Android Studio
2116
- `);
2117
- const gradleWrapperProps = `distributionBase=GRADLE_USER_HOME
2118
- distributionPath=wrapper/dists
2119
- distributionUrl=https\\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
2120
- networkTimeout=10000
2121
- validateDistributionUrl=true
2122
- zipStoreBase=GRADLE_USER_HOME
2123
- zipStorePath=wrapper/dists
2124
- `;
2125
- writeFileSync4(join3(output, "gradle/wrapper/gradle-wrapper.properties"), gradleWrapperProps);
2126
- const stringsXml = `<?xml version="1.0" encoding="utf-8"?>
2127
- <resources>
2128
- <string name="app_name">${escapeXml(name)}</string>
2129
- </resources>
2130
- `;
2131
- writeFileSync4(join3(output, "app/src/main/res/values/strings.xml"), stringsXml);
2132
- const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
2133
- <resources>
2134
- <color name="primary">#1a1a2e</color>
2135
- <color name="primary_dark">#16162b</color>
2136
- <color name="accent">#4ade80</color>
2137
- <color name="background">${config.backgroundColor}</color>
2138
- </resources>
2139
- `;
2140
- writeFileSync4(join3(output, "app/src/main/res/values/colors.xml"), colorsXml);
2141
- const appIconXml = `<?xml version="1.0" encoding="utf-8"?>
2142
- <vector xmlns:android="http://schemas.android.com/apk/res/android"
2143
- android:width="108dp"
2144
- android:height="108dp"
2145
- android:viewportWidth="108"
2146
- android:viewportHeight="108">
2147
- <path android:fillColor="${config.backgroundColor}" android:pathData="M0,0h108v108h-108z" />
2148
- <path android:fillColor="#10B981" android:pathData="M18,78L42,42l12,18l12,-12l24,30z" />
2149
- </vector>
2150
- `;
2151
- if (config.appIconPath) {
2152
- cpSync3(config.appIconPath, join3(output, `app/src/main/res/drawable/craft_app_icon${appIconExtension}`));
2153
- } else {
2154
- writeFileSync4(join3(output, "app/src/main/res/drawable/craft_app_icon.xml"), appIconXml);
2155
- }
2156
- const themesXml = `<?xml version="1.0" encoding="utf-8"?>
2157
- <resources>
2158
- <style name="Theme.CraftApp" parent="Theme.Material3.DayNight.NoActionBar">
2159
- <item name="android:statusBarColor">@color/primary_dark</item>
2160
- <item name="android:navigationBarColor">@color/primary</item>
2161
- <item name="android:windowBackground">@color/background</item>
2162
- </style>
2163
- </resources>
2164
- `;
2165
- writeFileSync4(join3(output, "app/src/main/res/values/themes.xml"), themesXml);
2166
- const activityMainXml = `<?xml version="1.0" encoding="utf-8"?>
2167
- <androidx.coordinatorlayout.widget.CoordinatorLayout
2168
- xmlns:android="http://schemas.android.com/apk/res/android"
2169
- android:layout_width="match_parent"
2170
- android:layout_height="match_parent"
2171
- android:fitsSystemWindows="true">
2172
-
2173
- <WebView
2174
- android:id="@+id/webview"
2175
- android:layout_width="match_parent"
2176
- android:layout_height="match_parent" />
2177
-
2178
- </androidx.coordinatorlayout.widget.CoordinatorLayout>
2179
- `;
2180
- writeFileSync4(join3(output, "app/src/main/res/layout/activity_main.xml"), activityMainXml);
2181
- const placeholderHtml = `<!DOCTYPE html>
2182
- <html>
2183
- <head>
2184
- <meta charset="UTF-8">
2185
- <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
2186
- <title>${escapeXml(name)}</title>
2187
- <style>
2188
- * { margin: 0; padding: 0; box-sizing: border-box; }
2189
- body {
2190
- font-family: 'Roboto', sans-serif;
2191
- background: ${config.backgroundColor};
2192
- color: white;
2193
- min-height: 100vh;
2194
- display: flex;
2195
- justify-content: center;
2196
- align-items: center;
2197
- }
2198
- .container { text-align: center; padding: 2rem; }
2199
- h1 { font-size: 2.5rem; margin-bottom: 1rem; }
2200
- p { opacity: 0.7; }
2201
- .ready { color: #4ade80; font-size: 0.9rem; margin-top: 2rem; }
2202
- </style>
2203
- </head>
2204
- <body>
2205
- <div class="container">
2206
- <h1>\u26A1 ${escapeXml(name)}</h1>
2207
- <p>Built with Craft Android</p>
2208
- <p class="ready" id="status">Waiting for Craft bridge...</p>
2209
- </div>
2210
- <script>
2211
- window.addEventListener('craftReady', (e) => {
2212
- document.getElementById('status').textContent = \`\u2713 Craft bridge ready (platform: \${e.detail.platform})\`;
2213
- console.log('Craft capabilities:', e.detail.capabilities);
2214
- });
2215
- </script>
2216
- </body>
2217
- </html>`;
2218
- writeFileSync4(join3(output, "app/src/main/assets/index.html"), placeholderHtml);
2219
- const runtimeDir = resolveRuntimeDir2(options.runtimeDir);
2220
- if (runtimeDir) {
2221
- installRuntime2(output, runtimeDir);
2222
- console.log(" Installed the Zig runtime from", runtimeDir);
2223
- }
2224
- console.log("\u2705 Project initialized");
2225
- console.log("");
2226
- console.log("Next steps:");
2227
- console.log(` 1. cd ${output}`);
2228
- console.log(" 2. Add your web content to app/src/main/assets/index.html");
2229
- console.log(" 3. Run: craft android build");
2230
- console.log(" 4. Run: craft android open");
2231
- console.log("");
2232
- }
2233
- async function build2(options) {
2234
- const { htmlPath, devServer, output, release, compile = true } = options;
2235
- console.log(`
2236
- \uD83D\uDCE6 Building Craft Android project...`);
2237
- const configPath = join3(output, "craft.config.json");
2238
- if (!existsSync5(configPath)) {
2239
- throw new Error(`No craft.config.json found in ${output}. Run 'craft android init' first.`);
2240
- }
2241
- const config = JSON.parse(readFileSync4(configPath, "utf-8"));
2242
- if (existsSync5(join3(output, "app/src/main/jniLibs"))) {
2243
- const runtimeDir = resolveRuntimeDir2(options.runtimeDir);
2244
- if (runtimeDir) {
2245
- installRuntime2(output, runtimeDir);
2246
- console.log(" Refreshed the Zig runtime from", runtimeDir);
2247
- } else {
2248
- console.log(" Keeping the Zig runtime installed at init (no runtime directory configured)");
2249
- }
2250
- }
2251
- if (devServer) {
2252
- const url = androidWebUrl(devServer, "Android dev server URL");
2253
- config.devServerURL = url.toString();
2254
- config.trustedOrigins = [...new Set([
2255
- ...(config.trustedOrigins ?? []).map((value) => androidWebUrl(value, "Android trusted origin").origin),
2256
- url.origin
2257
- ])];
2258
- writeAndroidConfig(output, config);
2259
- console.log(` Dev server: ${devServer}`);
2260
- }
2261
- if (htmlPath) {
2262
- syncAndroidWebAssets(htmlPath, output);
2263
- config.hasBundledFallback = Boolean(devServer);
2264
- writeAndroidConfig(output, config);
2265
- console.log(` Synced: ${htmlPath} \u2192 assets/`);
2266
- }
2267
- if (!compile)
2268
- return;
2269
- const gradlewPath = join3(output, "gradlew");
2270
- if (!existsSync5(gradlewPath)) {
2271
- console.log(" Generating Gradle wrapper...");
2272
- try {
2273
- await $2`cd ${output} && gradle wrapper`.quiet();
2274
- } catch (error) {
2275
- throw new Error("Gradle is unavailable. Install Android Studio or Gradle before building Android.", { cause: error });
2276
- }
2277
- }
2278
- const buildType = release ? "assembleRelease" : "assembleDebug";
2279
- console.log(` Building ${release ? "release" : "debug"} APK...`);
2280
- try {
2281
- await $2`cd ${output} && ./gradlew ${buildType}`;
2282
- const apkPath = release ? "app/build/outputs/apk/release/app-release.apk" : "app/build/outputs/apk/debug/app-debug.apk";
2283
- console.log(`\u2705 APK built: ${apkPath}`);
2284
- } catch (error) {
2285
- console.error("Build failed. Open in Android Studio for details.");
2286
- throw error;
2287
- }
2288
- console.log("");
2289
- }
2290
- async function open2(options) {
2291
- const { output } = options;
2292
- if (!existsSync5(output)) {
2293
- throw new Error(`No Android project found in ${output}. Run 'craft android init' first.`);
2294
- }
2295
- console.log(`\uD83D\uDE80 Opening Android project in Android Studio...`);
2296
- try {
2297
- await $2`open -a "Android Studio" ${output}`.quiet();
2298
- } catch {
2299
- try {
2300
- await $2`studio ${output}`.quiet();
2301
- } catch {
2302
- console.log("\u26A0\uFE0F Android Studio not found.");
2303
- console.log(" Please open the project manually in Android Studio:");
2304
- console.log(` ${output}`);
2305
- }
2306
- }
2307
- }
2308
- async function run2(options) {
2309
- const { device, output } = options;
2310
- await build2({ output });
2311
- console.log("\uD83D\uDCF1 Installing and running on device...");
2312
- try {
2313
- const apkPath = join3(output, "app/build/outputs/apk/debug/app-debug.apk");
2314
- if (device) {
2315
- await $2`adb -s ${device} install -r ${apkPath}`;
2316
- } else {
2317
- await $2`adb install -r ${apkPath}`;
2318
- }
2319
- const config = JSON.parse(readFileSync4(join3(output, "craft.config.json"), "utf-8"));
2320
- const launchCmd = `${config.packageName}/.MainActivity`;
2321
- if (device) {
2322
- await $2`adb -s ${device} shell am start -n ${launchCmd}`;
2323
- } else {
2324
- await $2`adb shell am start -n ${launchCmd}`;
2325
- }
2326
- console.log("\u2705 App launched");
2327
- } catch (error) {
2328
- console.error("Failed to install/run. Make sure:");
2329
- console.error(" 1. ADB is installed and in PATH");
2330
- console.error(" 2. A device/emulator is connected (run: adb devices)");
2331
- throw error;
2332
- }
2333
- }
2334
- var $2, ANDROID_PROMISE_RUNTIME = `
2335
- if (window.__craftRejectPendingPromises) {
2336
- window.__craftRejectPendingPromises('Android bridge reinitialized');
2337
- }
2338
- if (window.__craftRejectPermissionRequests) {
2339
- window.__craftRejectPermissionRequests('Android bridge reinitialized');
2340
- }
2341
- window.__craftPromiseRuntimeClosed = false;
2342
- window.__craftPendingPromises = Object.create(null);
2343
- window.__craftPromise = function(channel, resolveName, rejectName, invoke, timeoutMs, timeoutError) {
2344
- if (window.__craftPromiseRuntimeClosed) {
2345
- return Promise.reject(new Error('Android bridge is closed'));
2346
- }
2347
- if (window.__craftPendingPromises[channel]) {
2348
- return Promise.reject(new Error('A '.concat(channel, ' request is already in progress')));
2349
- }
2350
-
2351
- return new Promise(function(resolve, reject) {
2352
- var entry = {settled: false, timer: null, settle: null};
2353
- var resolveCallback;
2354
- var rejectCallback;
2355
-
2356
- entry.settle = function(succeeded, value) {
2357
- if (entry.settled || window.__craftPendingPromises[channel] !== entry) return;
2358
- entry.settled = true;
2359
- if (entry.timer !== null) clearTimeout(entry.timer);
2360
- if (window[resolveName] === resolveCallback) window[resolveName] = null;
2361
- if (window[rejectName] === rejectCallback) window[rejectName] = null;
2362
- delete window.__craftPendingPromises[channel];
2363
- if (succeeded) resolve(value);
2364
- else reject(value);
2365
- };
2366
-
2367
- resolveCallback = function(value) {
2368
- entry.settle(true, value);
2369
- };
2370
- rejectCallback = function(error) {
2371
- entry.settle(false, error);
2372
- };
2373
- window[resolveName] = resolveCallback;
2374
- window[rejectName] = rejectCallback;
2375
- window.__craftPendingPromises[channel] = entry;
2376
-
2377
- if (timeoutMs > 0) {
2378
- entry.timer = setTimeout(function() {
2379
- entry.settle(false, timeoutError || new Error(channel.concat(' request timed out')));
2380
- }, timeoutMs);
2381
- }
2382
-
2383
- try {
2384
- invoke();
2385
- }
2386
- catch (error) {
2387
- entry.settle(false, error);
2388
- }
2389
- });
2390
- };
2391
-
2392
- window.__craftRejectPendingPromises = function(message) {
2393
- window.__craftPromiseRuntimeClosed = true;
2394
- Object.keys(window.__craftPendingPromises).forEach(function(channel) {
2395
- var entry = window.__craftPendingPromises[channel];
2396
- if (entry) entry.settle(false, new Error(message || 'Android bridge closed'));
2397
- });
2398
- };
2399
- `, TEMPLATES_DIR2, LOCAL_DEVELOPMENT_HOSTS, KOTLIN_KEYWORDS, ANDROID_MIN_SDK = 26, DEFAULT_CONFIG2, RUNTIME_ABIS;
2400
- var init_src2 = __esm(() => {
2401
- ({ $: $2 } = globalThis.Bun);
2402
- TEMPLATES_DIR2 = join3(dirname2(import.meta.dir), "templates");
2403
- LOCAL_DEVELOPMENT_HOSTS = new Set(["localhost", "127.0.0.1", "10.0.2.2"]);
2404
- KOTLIN_KEYWORDS = new Set([
2405
- "as",
2406
- "break",
2407
- "class",
2408
- "continue",
2409
- "do",
2410
- "else",
2411
- "false",
2412
- "for",
2413
- "fun",
2414
- "if",
2415
- "in",
2416
- "interface",
2417
- "is",
2418
- "null",
2419
- "object",
2420
- "package",
2421
- "return",
2422
- "super",
2423
- "this",
2424
- "throw",
2425
- "true",
2426
- "try",
2427
- "typealias",
2428
- "typeof",
2429
- "val",
2430
- "var",
2431
- "when",
2432
- "while"
2433
- ]);
2434
- DEFAULT_CONFIG2 = {
2435
- version: "1.0.0",
2436
- versionCode: 1,
2437
- darkMode: true,
2438
- backgroundColor: "#0b1712",
2439
- enableSpeechRecognition: false,
2440
- enableHaptics: false,
2441
- enableShare: false,
2442
- enableCamera: false,
2443
- enableBiometric: false,
2444
- enablePushNotifications: false,
2445
- enableSecureStorage: false,
2446
- enableGeolocation: false,
2447
- enableBackgroundLocation: false,
2448
- enableHealthConnect: false,
2449
- enableKeepAwake: false,
2450
- enableDeepLinks: false,
2451
- urlSchemes: [],
2452
- trustedOrigins: [],
2453
- minSdk: ANDROID_MIN_SDK,
2454
- compileSdk: 36,
2455
- targetSdk: 35
2456
- };
2457
- RUNTIME_ABIS = ["arm64-v8a", "x86_64"];
2458
- });
2459
-
2460
1142
  // ../../node_modules/@stacksjs/clapp/dist/index.js
2461
1143
  import { existsSync } from "fs";
2462
1144
  import fs from "fs/promises";
@@ -4541,7 +3223,7 @@ var prefix = getPrefix();
4541
3223
 
4542
3224
  // bin/cli.ts
4543
3225
  import { spawn as spawn2 } from "child_process";
4544
- import { existsSync as existsSync6 } from "fs";
3226
+ import { existsSync as existsSync4 } from "fs";
4545
3227
  import process6 from "process";
4546
3228
 
4547
3229
  // src/binary-resolver.ts
@@ -4621,7 +3303,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
4621
3303
  `);
4622
3304
  }
4623
3305
  // package.json
4624
- var version = "0.0.104";
3306
+ var version = "0.0.107";
4625
3307
 
4626
3308
  // bin/cli.ts
4627
3309
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
@@ -4630,16 +3312,18 @@ if (spawnedFrom) {
4630
3312
  process6.exit(1);
4631
3313
  }
4632
3314
  var cli = new CLI("craft");
3315
+ var iosModuleURL = new URL("../dist/ios/src/index.js", import.meta.url).href;
3316
+ var androidModuleURL = new URL("../dist/android/src/index.js", import.meta.url).href;
4633
3317
  async function runCraftBinary(args) {
4634
3318
  const craftPath = resolveCraftBinary();
4635
- return new Promise((resolve3, reject) => {
3319
+ return new Promise((resolve, reject) => {
4636
3320
  const proc = spawn2(craftPath, args, {
4637
3321
  stdio: "inherit",
4638
3322
  env: { ...process6.env, [CRAFT_CLI_SPAWN_MARKER]: craftPath }
4639
3323
  });
4640
3324
  proc.on("exit", (code) => {
4641
3325
  if (code === 0 || code === null)
4642
- resolve3();
3326
+ resolve();
4643
3327
  else
4644
3328
  reject(new Error(`Craft exited with code ${code}`));
4645
3329
  });
@@ -4733,7 +3417,7 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
4733
3417
  console.log(`Platforms: ${platforms.join(", ")}`);
4734
3418
  console.log(`Mode: ${options?.release ? "Release" : "Debug"}
4735
3419
  `);
4736
- const { existsSync: existsSync7 } = await import("fs");
3420
+ const { existsSync: existsSync5 } = await import("fs");
4737
3421
  const { spawn: spawn3 } = await import("child_process");
4738
3422
  for (const platform of platforms) {
4739
3423
  console.log(`\uD83D\uDCE6 Building for ${platform}...`);
@@ -4744,9 +3428,9 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
4744
3428
  continue;
4745
3429
  }
4746
3430
  const iosDir = "./ios";
4747
- if (existsSync7(iosDir)) {
3431
+ if (existsSync5(iosDir)) {
4748
3432
  const buildType = options?.release ? "Release" : "Debug";
4749
- await new Promise((resolve3, reject) => {
3433
+ await new Promise((resolve, reject) => {
4750
3434
  const proc = spawn3("xcodebuild", [
4751
3435
  "-project",
4752
3436
  `${iosDir}/App.xcodeproj`,
@@ -4758,7 +3442,7 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
4758
3442
  "iphoneos",
4759
3443
  "build"
4760
3444
  ], { stdio: "inherit" });
4761
- proc.on("exit", (code) => code === 0 ? resolve3() : reject(new Error(`Exit ${code}`)));
3445
+ proc.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Exit ${code}`)));
4762
3446
  proc.on("error", reject);
4763
3447
  });
4764
3448
  console.log(` \u2705 iOS ${buildType} build complete`);
@@ -4767,14 +3451,14 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
4767
3451
  }
4768
3452
  } else if (platform === "android") {
4769
3453
  const androidDir = "./android";
4770
- if (existsSync7(androidDir)) {
3454
+ if (existsSync5(androidDir)) {
4771
3455
  const task = options?.release ? "assembleRelease" : "assembleDebug";
4772
- await new Promise((resolve3, reject) => {
3456
+ await new Promise((resolve, reject) => {
4773
3457
  const proc = spawn3("./gradlew", [task], {
4774
3458
  cwd: androidDir,
4775
3459
  stdio: "inherit"
4776
3460
  });
4777
- proc.on("exit", (code) => code === 0 ? resolve3() : reject(new Error(`Exit ${code}`)));
3461
+ proc.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Exit ${code}`)));
4778
3462
  proc.on("error", reject);
4779
3463
  });
4780
3464
  console.log(` \u2705 Android ${options?.release ? "Release" : "Debug"} build complete`);
@@ -4782,9 +3466,9 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
4782
3466
  console.log(` \u26A0\uFE0F No Android project found. Run: craft android init`);
4783
3467
  }
4784
3468
  } else if (platform === "macos" || platform === "windows" || platform === "linux") {
4785
- const zigDir = existsSync7("./packages/zig") ? "./packages/zig" : ".";
3469
+ const zigDir = existsSync5("./packages/zig") ? "./packages/zig" : ".";
4786
3470
  const optimizeFlag = options?.release ? "-Doptimize=ReleaseSafe" : "";
4787
- await new Promise((resolve3, reject) => {
3471
+ await new Promise((resolve, reject) => {
4788
3472
  const args = ["build"];
4789
3473
  if (optimizeFlag)
4790
3474
  args.push(optimizeFlag);
@@ -4792,7 +3476,7 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
4792
3476
  cwd: zigDir,
4793
3477
  stdio: "inherit"
4794
3478
  });
4795
- proc.on("exit", (code) => code === 0 ? resolve3() : reject(new Error(`Exit ${code}`)));
3479
+ proc.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Exit ${code}`)));
4796
3480
  proc.on("error", reject);
4797
3481
  });
4798
3482
  console.log(` \u2705 ${platform} build complete`);
@@ -4831,7 +3515,7 @@ cli.command("package", "Create installers for your Craft application").option("-
4831
3515
  const { packageApp: packageApp2 } = packageModule;
4832
3516
  let config;
4833
3517
  if (options?.config) {
4834
- if (!existsSync6(options.config)) {
3518
+ if (!existsSync4(options.config)) {
4835
3519
  console.error(`\u274C Config file not found: ${options.config}`);
4836
3520
  process6.exit(1);
4837
3521
  }
@@ -4917,20 +3601,22 @@ cli.command("package", "Create installers for your Craft application").option("-
4917
3601
  process6.exit(1);
4918
3602
  }
4919
3603
  });
4920
- cli.command("ios init <name>", "Initialize a new iOS project").option("--bundle-id <id>", "Bundle identifier (e.g., com.example.app)").option("--team-id <id>", "Apple Developer Team ID").option("-o, --output <dir>", "Output directory", { default: "./ios" }).example("craft ios init MyApp").example("craft ios init MyApp --bundle-id com.example.myapp").action(async (name, options) => {
4921
- const iosModule = await Promise.resolve().then(() => (init_src(), exports_src));
3604
+ cli.command("ios init <name>", "Initialize a new iOS project").option("--bundle-id <id>", "Bundle identifier (e.g., com.example.app)").option("--team-id <id>", "Apple Developer Team ID").option("--renderer <kind>", "iOS renderer: web (default) or native").option("-o, --output <dir>", "Output directory", { default: "./ios" }).example("craft ios init MyApp").example("craft ios init MyApp --bundle-id com.example.myapp").action(async (name, options) => {
3605
+ const iosModule = await import(iosModuleURL);
4922
3606
  await iosModule.init({
4923
3607
  name,
4924
3608
  bundleId: options?.bundleId,
4925
3609
  teamId: options?.teamId,
4926
- output: options?.output || "./ios"
3610
+ output: options?.output || "./ios",
3611
+ config: options?.renderer ? { renderer: options.renderer } : undefined
4927
3612
  });
4928
3613
  });
4929
- cli.command("ios build", "Build iOS project").option("--html-path <path>", "Path to HTML file").option("-d, --dev-server <url>", "Development server URL").option("-o, --output <dir>", "iOS project directory", { default: "./ios" }).option("-w, --watch", "Watch for file changes and rebuild").example("craft ios build").example("craft ios build --html-path ./dist/index.html").example("craft ios build --dev-server http://localhost:3456").example("craft ios build --watch").action(async (options) => {
4930
- const iosModule = await Promise.resolve().then(() => (init_src(), exports_src));
3614
+ cli.command("ios build", "Build iOS project").option("--html-path <path>", "Path to HTML file").option("--native-bundle <path>", "JavaScript bundle compiled by stx-native").option("-d, --dev-server <url>", "Development server URL").option("-o, --output <dir>", "iOS project directory", { default: "./ios" }).option("-w, --watch", "Watch for file changes and rebuild").example("craft ios build").example("craft ios build --html-path ./dist/index.html").example("craft ios build --native-bundle ./screen.js").example("craft ios build --dev-server http://localhost:3456").example("craft ios build --watch").action(async (options) => {
3615
+ const iosModule = await import(iosModuleURL);
4931
3616
  const doBuild = async () => {
4932
3617
  await iosModule.build({
4933
3618
  htmlPath: options?.htmlPath,
3619
+ nativeBundlePath: options?.nativeBundle,
4934
3620
  devServer: options?.devServer,
4935
3621
  output: options?.output || "./ios"
4936
3622
  });
@@ -4941,8 +3627,8 @@ cli.command("ios build", "Build iOS project").option("--html-path <path>", "Path
4941
3627
  \uD83D\uDC40 Watching for changes...
4942
3628
  `);
4943
3629
  const { watch } = await import("fs");
4944
- const { dirname: dirname3 } = await import("path");
4945
- const watchPath = options?.htmlPath ? dirname3(options.htmlPath) : ".";
3630
+ const { dirname } = await import("path");
3631
+ const watchPath = options?.htmlPath ? dirname(options.htmlPath) : ".";
4946
3632
  watch(watchPath, { recursive: true }, async (event, filename) => {
4947
3633
  if (filename && (filename.endsWith(".html") || filename.endsWith(".js") || filename.endsWith(".css"))) {
4948
3634
  console.log(`
@@ -4954,31 +3640,33 @@ cli.command("ios build", "Build iOS project").option("--html-path <path>", "Path
4954
3640
  }
4955
3641
  });
4956
3642
  cli.command("ios open", "Open iOS project in Xcode").option("-o, --output <dir>", "iOS project directory", { default: "./ios" }).action(async (options) => {
4957
- const iosModule = await Promise.resolve().then(() => (init_src(), exports_src));
3643
+ const iosModule = await import(iosModuleURL);
4958
3644
  await iosModule.open({
4959
3645
  output: options?.output || "./ios"
4960
3646
  });
4961
3647
  });
4962
3648
  cli.command("ios run", "Build and run on iOS device or simulator").option("-s, --simulator", "Run on simulator instead of device").option("-o, --output <dir>", "iOS project directory", { default: "./ios" }).example("craft ios run").example("craft ios run --simulator").action(async (options) => {
4963
- const iosModule = await Promise.resolve().then(() => (init_src(), exports_src));
3649
+ const iosModule = await import(iosModuleURL);
4964
3650
  await iosModule.run({
4965
3651
  simulator: options?.simulator || false,
4966
3652
  output: options?.output || "./ios"
4967
3653
  });
4968
3654
  });
4969
- cli.command("android init <name>", "Initialize a new Android project").option("--package <name>", "Package name (e.g., com.example.app)").option("-o, --output <dir>", "Output directory", { default: "./android" }).example("craft android init MyApp").example("craft android init MyApp --package com.example.myapp").action(async (name, options) => {
4970
- const androidModule = await Promise.resolve().then(() => (init_src2(), exports_src2));
3655
+ cli.command("android init <name>", "Initialize a new Android project").option("--package <name>", "Package name (e.g., com.example.app)").option("--renderer <kind>", "Android renderer: web (default) or native").option("-o, --output <dir>", "Output directory", { default: "./android" }).example("craft android init MyApp").example("craft android init MyApp --package com.example.myapp").action(async (name, options) => {
3656
+ const androidModule = await import(androidModuleURL);
4971
3657
  await androidModule.init({
4972
3658
  name,
4973
3659
  packageName: options?.package,
4974
- output: options?.output || "./android"
3660
+ output: options?.output || "./android",
3661
+ config: options?.renderer ? { renderer: options.renderer } : undefined
4975
3662
  });
4976
3663
  });
4977
- cli.command("android build", "Build Android project").option("--html-path <path>", "Path to HTML file").option("-d, --dev-server <url>", "Development server URL").option("-o, --output <dir>", "Android project directory", { default: "./android" }).option("--release", "Build release APK").option("-w, --watch", "Watch for file changes and rebuild").example("craft android build").example("craft android build --release").example("craft android build --watch").action(async (options) => {
4978
- const androidModule = await Promise.resolve().then(() => (init_src2(), exports_src2));
3664
+ cli.command("android build", "Build Android project").option("--html-path <path>", "Path to HTML file").option("--native-bundle <path>", "JavaScript bundle compiled by stx-native").option("-d, --dev-server <url>", "Development server URL").option("-o, --output <dir>", "Android project directory", { default: "./android" }).option("--release", "Build release APK").option("-w, --watch", "Watch for file changes and rebuild").example("craft android build").example("craft android build --release").example("craft android build --native-bundle ./screen.js").example("craft android build --watch").action(async (options) => {
3665
+ const androidModule = await import(androidModuleURL);
4979
3666
  const doBuild = async () => {
4980
3667
  await androidModule.build({
4981
3668
  htmlPath: options?.htmlPath,
3669
+ nativeBundlePath: options?.nativeBundle,
4982
3670
  devServer: options?.devServer,
4983
3671
  output: options?.output || "./android",
4984
3672
  release: options?.release || false
@@ -4990,8 +3678,8 @@ cli.command("android build", "Build Android project").option("--html-path <path>
4990
3678
  \uD83D\uDC40 Watching for changes...
4991
3679
  `);
4992
3680
  const { watch } = await import("fs");
4993
- const { dirname: dirname3 } = await import("path");
4994
- const watchPath = options?.htmlPath ? dirname3(options.htmlPath) : ".";
3681
+ const { dirname } = await import("path");
3682
+ const watchPath = options?.htmlPath ? dirname(options.htmlPath) : ".";
4995
3683
  watch(watchPath, { recursive: true }, async (event, filename) => {
4996
3684
  if (filename && (filename.endsWith(".html") || filename.endsWith(".js") || filename.endsWith(".css"))) {
4997
3685
  console.log(`
@@ -5003,13 +3691,13 @@ cli.command("android build", "Build Android project").option("--html-path <path>
5003
3691
  }
5004
3692
  });
5005
3693
  cli.command("android open", "Open Android project in Android Studio").option("-o, --output <dir>", "Android project directory", { default: "./android" }).action(async (options) => {
5006
- const androidModule = await Promise.resolve().then(() => (init_src2(), exports_src2));
3694
+ const androidModule = await import(androidModuleURL);
5007
3695
  await androidModule.open({
5008
3696
  output: options?.output || "./android"
5009
3697
  });
5010
3698
  });
5011
3699
  cli.command("android run", "Build and run on Android device or emulator").option("-d, --device <id>", "Target device ID").option("-o, --output <dir>", "Android project directory", { default: "./android" }).example("craft android run").example("craft android run --device emulator-5554").action(async (options) => {
5012
- const androidModule = await Promise.resolve().then(() => (init_src2(), exports_src2));
3700
+ const androidModule = await import(androidModuleURL);
5013
3701
  await androidModule.run({
5014
3702
  device: options?.device,
5015
3703
  output: options?.output || "./android"
@@ -5036,30 +3724,30 @@ cli.command("preview [path]", "Preview app in browser before building native").o
5036
3724
  });
5037
3725
  });
5038
3726
  cli.command("publish", "Publish app to App Store or Play Store").option("--ios", "Publish to App Store (TestFlight)").option("--android", "Publish to Play Store").option("--api-key <path>", "App Store Connect API key path").option("--service-account <path>", "Google Play service account JSON").option("-o, --output <dir>", "Project directory", { default: "." }).example("craft publish --ios").example("craft publish --android").action(async (options) => {
5039
- const { existsSync: existsSync7 } = await import("fs");
5040
- const { join: join4 } = await import("path");
5041
- const { $: $3 } = await Promise.resolve(globalThis.Bun);
3727
+ const { existsSync: existsSync5 } = await import("fs");
3728
+ const { join: join2 } = await import("path");
3729
+ const { $ } = await Promise.resolve(globalThis.Bun);
5042
3730
  if (options?.ios) {
5043
3731
  console.log(`
5044
3732
  \uD83D\uDCF1 Publishing to App Store (TestFlight)...
5045
3733
  `);
5046
- const iosDir = join4(options?.output || ".", "ios");
5047
- if (!existsSync7(iosDir)) {
3734
+ const iosDir = join2(options?.output || ".", "ios");
3735
+ if (!existsSync5(iosDir)) {
5048
3736
  console.error("\u274C No iOS project found. Run: craft ios init");
5049
3737
  process6.exit(1);
5050
3738
  }
5051
3739
  console.log("1. Building archive...");
5052
3740
  try {
5053
- await $3`cd ${iosDir} && xcodebuild -scheme App -configuration Release -archivePath ./build/App.xcarchive archive`;
3741
+ await $`cd ${iosDir} && xcodebuild -scheme App -configuration Release -archivePath ./build/App.xcarchive archive`;
5054
3742
  console.log("\u2705 Archive created");
5055
3743
  console.log("2. Exporting IPA...");
5056
- await $3`cd ${iosDir} && xcodebuild -exportArchive -archivePath ./build/App.xcarchive -exportPath ./build -exportOptionsPlist ExportOptions.plist`;
3744
+ await $`cd ${iosDir} && xcodebuild -exportArchive -archivePath ./build/App.xcarchive -exportPath ./build -exportOptionsPlist ExportOptions.plist`;
5057
3745
  console.log("\u2705 IPA exported");
5058
3746
  console.log("3. Uploading to TestFlight...");
5059
3747
  if (options?.apiKey) {
5060
- await $3`xcrun altool --upload-app -f ${iosDir}/build/*.ipa --apiKey ${options.apiKey} --type ios`;
3748
+ await $`xcrun altool --upload-app -f ${iosDir}/build/*.ipa --apiKey ${options.apiKey} --type ios`;
5061
3749
  } else {
5062
- await $3`xcrun altool --upload-app -f ${iosDir}/build/*.ipa --type ios`;
3750
+ await $`xcrun altool --upload-app -f ${iosDir}/build/*.ipa --type ios`;
5063
3751
  }
5064
3752
  console.log("\u2705 Uploaded to TestFlight!");
5065
3753
  } catch (error) {
@@ -5073,16 +3761,16 @@ cli.command("publish", "Publish app to App Store or Play Store").option("--ios",
5073
3761
  console.log(`
5074
3762
  \uD83E\uDD16 Publishing to Play Store...
5075
3763
  `);
5076
- const androidDir = join4(options?.output || ".", "android");
5077
- if (!existsSync7(androidDir)) {
3764
+ const androidDir = join2(options?.output || ".", "android");
3765
+ if (!existsSync5(androidDir)) {
5078
3766
  console.error("\u274C No Android project found. Run: craft android init");
5079
3767
  process6.exit(1);
5080
3768
  }
5081
3769
  console.log("1. Building release AAB...");
5082
3770
  try {
5083
- await $3`cd ${androidDir} && ./gradlew bundleRelease`;
3771
+ await $`cd ${androidDir} && ./gradlew bundleRelease`;
5084
3772
  console.log("\u2705 AAB created");
5085
- const aabPath = join4(androidDir, "app/build/outputs/bundle/release/app-release.aab");
3773
+ const aabPath = join2(androidDir, "app/build/outputs/bundle/release/app-release.aab");
5086
3774
  if (options?.serviceAccount) {
5087
3775
  console.log("2. Uploading to Play Store...");
5088
3776
  console.log("\u26A0\uFE0F Automatic Play Store upload requires fastlane or Google Play API integration.");
@@ -5121,31 +3809,31 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
5121
3809
  `);
5122
3810
  const bundleId = options?.bundleId || `com.example.${name.toLowerCase().replace(/[^a-z0-9]/g, "")}`;
5123
3811
  const appNameSlug = name.toLowerCase().replace(/[^a-z0-9]/g, "-");
5124
- const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync5, existsSync: existsSync7, readdirSync: readdirSync3, readFileSync: readFileSync5, cpSync: cpSync4 } = await import("fs");
5125
- const { join: join4, dirname: dirname3 } = await import("path");
3812
+ const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync3, existsSync: existsSync5, readdirSync: readdirSync2, readFileSync: readFileSync3, cpSync: cpSync2 } = await import("fs");
3813
+ const { join: join2, dirname } = await import("path");
5126
3814
  const replaceVars = (content) => {
5127
3815
  return content.replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{APP_NAME_SLUG\}\}/g, appNameSlug).replace(/\{\{BUNDLE_ID\}\}/g, bundleId).replace(/\{\{AUTHOR\}\}/g, "Developer");
5128
3816
  };
5129
3817
  const copyTemplate = async (templateName, destDir) => {
5130
- const templateDir = join4(import.meta.dir, "templates/projects", templateName);
5131
- if (!existsSync7(templateDir)) {
3818
+ const templateDir = join2(import.meta.dir, "templates/projects", templateName);
3819
+ if (!existsSync5(templateDir)) {
5132
3820
  console.log(` \u26A0\uFE0F Template '${templateName}' not found, using blank template`);
5133
3821
  return false;
5134
3822
  }
5135
3823
  const copyRecursive = (src, dest) => {
5136
- if (!existsSync7(dest)) {
5137
- mkdirSync4(dest, { recursive: true });
3824
+ if (!existsSync5(dest)) {
3825
+ mkdirSync2(dest, { recursive: true });
5138
3826
  }
5139
- const entries = readdirSync3(src, { withFileTypes: true });
3827
+ const entries = readdirSync2(src, { withFileTypes: true });
5140
3828
  for (const entry of entries) {
5141
- const srcPath = join4(src, entry.name);
5142
- const destPath = join4(dest, entry.name);
3829
+ const srcPath = join2(src, entry.name);
3830
+ const destPath = join2(dest, entry.name);
5143
3831
  if (entry.isDirectory()) {
5144
3832
  copyRecursive(srcPath, destPath);
5145
3833
  } else {
5146
- const content = readFileSync5(srcPath, "utf-8");
3834
+ const content = readFileSync3(srcPath, "utf-8");
5147
3835
  const processedContent = replaceVars(content);
5148
- writeFileSync5(destPath, processedContent);
3836
+ writeFileSync3(destPath, processedContent);
5149
3837
  }
5150
3838
  }
5151
3839
  };
@@ -5154,13 +3842,13 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
5154
3842
  };
5155
3843
  if (["blank", "tabs", "drawer", "dashboard"].includes(template)) {
5156
3844
  console.log(`\uD83D\uDCC1 Creating ${template} project from template...`);
5157
- if (!existsSync7(name)) {
5158
- mkdirSync4(name, { recursive: true });
3845
+ if (!existsSync5(name)) {
3846
+ mkdirSync2(name, { recursive: true });
5159
3847
  }
5160
3848
  const copied = await copyTemplate(template, name);
5161
3849
  if (!copied) {
5162
- mkdirSync4(join4(name, "src"), { recursive: true });
5163
- writeFileSync5(join4(name, "index.html"), replaceVars(`<!DOCTYPE html>
3850
+ mkdirSync2(join2(name, "src"), { recursive: true });
3851
+ writeFileSync3(join2(name, "index.html"), replaceVars(`<!DOCTYPE html>
5164
3852
  <html lang="en">
5165
3853
  <head>
5166
3854
  <meta charset="UTF-8">
@@ -5174,7 +3862,7 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
5174
3862
  </div>
5175
3863
  </body>
5176
3864
  </html>`));
5177
- writeFileSync5(join4(name, "package.json"), replaceVars(JSON.stringify({
3865
+ writeFileSync3(join2(name, "package.json"), replaceVars(JSON.stringify({
5178
3866
  name: "{{APP_NAME_SLUG}}",
5179
3867
  version: "1.0.0",
5180
3868
  private: true,
@@ -5188,13 +3876,13 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
5188
3876
  }
5189
3877
  }, null, 2)));
5190
3878
  }
5191
- pinCraftNativeDependency(join4(name, "package.json"), version);
3879
+ pinCraftNativeDependency(join2(name, "package.json"), version);
5192
3880
  console.log(`\u2705 ${template} project created`);
5193
3881
  }
5194
3882
  if (template === "desktop" || template === "all") {
5195
3883
  console.log("\uD83D\uDCC1 Creating desktop project structure...");
5196
- if (!existsSync7(name)) {
5197
- mkdirSync4(name, { recursive: true });
3884
+ if (!existsSync5(name)) {
3885
+ mkdirSync2(name, { recursive: true });
5198
3886
  }
5199
3887
  const configContent = `import type { CraftConfig } from 'craft-native'
5200
3888
 
@@ -5208,7 +3896,7 @@ export default {
5208
3896
  },
5209
3897
  } satisfies CraftConfig
5210
3898
  `;
5211
- writeFileSync5(`${name}/craft.config.ts`, configContent);
3899
+ writeFileSync3(`${name}/craft.config.ts`, configContent);
5212
3900
  const htmlContent = `<!DOCTYPE html>
5213
3901
  <html lang="en">
5214
3902
  <head>
@@ -5239,7 +3927,7 @@ export default {
5239
3927
  </body>
5240
3928
  </html>
5241
3929
  `;
5242
- writeFileSync5(`${name}/index.html`, htmlContent);
3930
+ writeFileSync3(`${name}/index.html`, htmlContent);
5243
3931
  const packageJson = {
5244
3932
  name: appNameSlug,
5245
3933
  version: "0.1.0",
@@ -5255,13 +3943,13 @@ export default {
5255
3943
  "craft-native": `^${version}`
5256
3944
  }
5257
3945
  };
5258
- writeFileSync5(`${name}/package.json`, JSON.stringify(packageJson, null, 2));
3946
+ writeFileSync3(`${name}/package.json`, JSON.stringify(packageJson, null, 2));
5259
3947
  pinCraftNativeDependency(`${name}/package.json`, version);
5260
3948
  console.log("\u2705 Desktop project created");
5261
3949
  }
5262
3950
  if (template === "ios" || template === "all") {
5263
3951
  console.log("\uD83D\uDCF1 Creating iOS project...");
5264
- const iosModule = await Promise.resolve().then(() => (init_src(), exports_src));
3952
+ const iosModule = await import(iosModuleURL);
5265
3953
  await iosModule.init({
5266
3954
  name,
5267
3955
  bundleId: options?.bundleId,
@@ -5271,7 +3959,7 @@ export default {
5271
3959
  }
5272
3960
  if (template === "android" || template === "all") {
5273
3961
  console.log("\uD83E\uDD16 Creating Android project...");
5274
- const androidModule = await Promise.resolve().then(() => (init_src2(), exports_src2));
3962
+ const androidModule = await import(androidModuleURL);
5275
3963
  await androidModule.init({
5276
3964
  name,
5277
3965
  packageName: options?.bundleId,
@@ -5298,4 +3986,7 @@ export default {
5298
3986
  });
5299
3987
  cli.version(version);
5300
3988
  cli.help();
3989
+ if ((process6.argv[2] === "ios" || process6.argv[2] === "android") && ["init", "build", "open", "run"].includes(process6.argv[3])) {
3990
+ process6.argv.splice(2, 2, `${process6.argv[2]} ${process6.argv[3]}`);
3991
+ }
5301
3992
  cli.parse();