craft-native 0.0.90 → 0.0.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/android/src/index.d.ts +36 -1
  2. package/dist/android/src/index.js +332 -44
  3. package/dist/android/src/promise-runtime.d.ts +3 -0
  4. package/dist/android/templates/CraftBridge.kt.template +2167 -1177
  5. package/dist/android/templates/CraftHealthConnect.kt.template +45 -6
  6. package/dist/android/templates/CraftHealthConnectStub.kt.template +7 -1
  7. package/dist/android/templates/CraftNative.kt.template +2250 -0
  8. package/dist/android/templates/LocationRecordingService.kt.template +34 -9
  9. package/dist/android/templates/MainActivity.kt.template +42 -8
  10. package/dist/android/templates/proguard-rules.pro.template +4 -1
  11. package/dist/android/templates/test-bridges.html +10 -33
  12. package/dist/api/index.d.ts +1 -1
  13. package/dist/api/ios-advanced.d.ts +8 -5
  14. package/dist/api/live-activity-handle.d.ts +6 -0
  15. package/dist/api/mobile.d.ts +25 -7
  16. package/dist/api/window.d.ts +2 -0
  17. package/dist/cli.js +452 -129
  18. package/dist/index.cjs +77 -22
  19. package/dist/index.js +77 -22
  20. package/dist/ios/src/index.d.ts +1 -1
  21. package/dist/ios/src/index.js +23 -5
  22. package/dist/ios/templates/CraftApp.swift +706 -92
  23. package/dist/ios/templates/CraftWatchApp.swift.template +8 -0
  24. package/dist/ios/templates/WatchApp.Info.plist.template +2 -0
  25. package/dist/ios/templates/WatchExtension.Info.plist.template +34 -0
  26. package/dist/ios/templates/project.yml.template +10 -4
  27. package/dist/mobile.js +52 -21
  28. package/dist/scaffold-version.d.ts +5 -0
  29. package/package.json +1 -1
  30. package/dist/android/templates/CraftBridgeExtensions.kt.template +0 -383
  31. package/dist/android/templates/CraftWidgetProvider.kt.template +0 -246
package/dist/cli.js CHANGED
@@ -48,10 +48,10 @@ import {
48
48
  lstatSync,
49
49
  mkdirSync,
50
50
  mkdtempSync,
51
- readFileSync,
51
+ readFileSync as readFileSync2,
52
52
  readdirSync,
53
53
  rmSync,
54
- writeFileSync
54
+ writeFileSync as writeFileSync2
55
55
  } from "fs";
56
56
  import { homedir, tmpdir } from "os";
57
57
  import { basename, join } from "path";
@@ -488,7 +488,7 @@ function createMacOSAppBundle(opts) {
488
488
  return { success: false, error: `Provisioning profile not found: ${provisioningProfile}` };
489
489
  copyFileSync(provisioningProfile, join(contents, "embedded.provisionprofile"));
490
490
  }
491
- writeFileSync(join(contents, "Info.plist"), macOSInfoPlist({ ...opts, iconName }));
491
+ writeFileSync2(join(contents, "Info.plist"), macOSInfoPlist({ ...opts, iconName }));
492
492
  return { success: true };
493
493
  } catch (error) {
494
494
  return { success: false, error: error.message };
@@ -626,7 +626,7 @@ async function createPKG(opts) {
626
626
  const bundleName = basename(opts.appBundlePath);
627
627
  cpSync(opts.appBundlePath, join(appsDir, bundleName), { recursive: true });
628
628
  const componentPlistPath = join(tempDir, "component.plist");
629
- writeFileSync(componentPlistPath, pkgbuildComponentPlist(`Applications/${bundleName}`));
629
+ writeFileSync2(componentPlistPath, pkgbuildComponentPlist(`Applications/${bundleName}`));
630
630
  const built = await runTool("pkgbuild", pkgbuildArguments({
631
631
  root,
632
632
  componentPlistPath,
@@ -716,7 +716,7 @@ async function createMSI(opts) {
716
716
  const wxsPath = join(tempDir, "installer.wxs");
717
717
  const wixobjPath = join(tempDir, "installer.wixobj");
718
718
  copyFileSync(opts.binaryPath, sourcePath);
719
- writeFileSync(wxsPath, renderWixSource(opts, binaryName));
719
+ writeFileSync2(wxsPath, renderWixSource(opts, binaryName));
720
720
  await runCommand("candle.exe", candleArguments(opts.architecture, wixobjPath, wxsPath), tempDir);
721
721
  await runCommand("light.exe", ["-nologo", "-sval", "-out", opts.outputPath, wixobjPath], tempDir);
722
722
  if (opts.certificatePath) {
@@ -738,9 +738,9 @@ async function createMSI(opts) {
738
738
  }
739
739
  async function createZIP(opts) {
740
740
  try {
741
- const data = new Uint8Array(readFileSync(opts.binaryPath));
741
+ const data = new Uint8Array(readFileSync2(opts.binaryPath));
742
742
  const zip = buildZip([{ name: `${opts.name}.exe`, data }]);
743
- writeFileSync(opts.outputPath, zip);
743
+ writeFileSync2(opts.outputPath, zip);
744
744
  return { success: true, outputPath: opts.outputPath };
745
745
  } catch (err) {
746
746
  return { success: false, error: `Failed to write ZIP: ${err.message}` };
@@ -788,7 +788,7 @@ Depends: ${opts.dependencies.join(", ")}
788
788
  Maintainer: ${maintainer}
789
789
  Description: ${description || opts.name}
790
790
  `;
791
- writeFileSync(join(debianDir, "control"), controlContent);
791
+ writeFileSync2(join(debianDir, "control"), controlContent);
792
792
  const desktopContent = `[Desktop Entry]
793
793
  Type=Application
794
794
  Name=${opts.name}
@@ -796,7 +796,7 @@ Exec=/usr/bin/${binaryName}
796
796
  Terminal=false
797
797
  Categories=Utility;
798
798
  `;
799
- writeFileSync(join(applicationsDir, `${binaryName}.desktop`), desktopContent);
799
+ writeFileSync2(join(applicationsDir, `${binaryName}.desktop`), desktopContent);
800
800
  const proc = spawn("dpkg-deb", ["--build", tempDir, opts.outputPath]);
801
801
  proc.on("close", (code) => {
802
802
  rmSync(tempDir, { recursive: true, force: true });
@@ -865,7 +865,7 @@ install -m 755 %{SOURCE0} %{buildroot}/usr/bin/${sanitizedName}
865
865
  /usr/bin/${sanitizedName}
866
866
  `;
867
867
  const specPath = join(specDir, `${opts.name.toLowerCase()}.spec`);
868
- writeFileSync(specPath, specContent);
868
+ writeFileSync2(specPath, specContent);
869
869
  const proc = spawn("rpmbuild", ["-bb", specPath]);
870
870
  proc.on("close", (code) => {
871
871
  if (code === 0) {
@@ -907,7 +907,7 @@ HERE=\${SELF%/*}
907
907
  export PATH="\${HERE}/usr/bin/:\${PATH}"
908
908
  exec "\${HERE}/usr/bin/${binaryName}" "$@"
909
909
  `;
910
- writeFileSync(join(appDirPath, "AppRun"), appRunContent);
910
+ writeFileSync2(join(appDirPath, "AppRun"), appRunContent);
911
911
  chmodSync(join(appDirPath, "AppRun"), 493);
912
912
  const desktopContent = `[Desktop Entry]
913
913
  Type=Application
@@ -917,7 +917,7 @@ Terminal=false
917
917
  Categories=Utility;
918
918
  Icon=${binaryName}
919
919
  `;
920
- writeFileSync(join(appDirPath, `${binaryName}.desktop`), desktopContent);
920
+ writeFileSync2(join(appDirPath, `${binaryName}.desktop`), desktopContent);
921
921
  if (opts.iconPath && existsSync3(opts.iconPath)) {
922
922
  copyFileSync(opts.iconPath, join(appDirPath, `${binaryName}.png`));
923
923
  } else {
@@ -992,7 +992,7 @@ Icon=${binaryName}
992
992
  96,
993
993
  130
994
994
  ]);
995
- writeFileSync(join(appDirPath, `${binaryName}.png`), placeholderPng);
995
+ writeFileSync2(join(appDirPath, `${binaryName}.png`), placeholderPng);
996
996
  }
997
997
  const proc = spawn("appimagetool", [appDirPath, opts.outputPath], {
998
998
  env: { ...process.env, ARCH: "x86_64" }
@@ -1071,7 +1071,7 @@ __export(exports_src, {
1071
1071
  build: () => build,
1072
1072
  bootSimulator: () => bootSimulator
1073
1073
  });
1074
- import { cpSync as cpSync2, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
1074
+ 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";
1075
1075
  import { dirname, join as join2, resolve } from "path";
1076
1076
  function renderDeviceFamilies(config) {
1077
1077
  const values = new Set(((config.deviceFamilies?.length) ? config.deviceFamilies : ["iphone", "ipad"]).map((family) => family === "ipad" ? "2" : "1"));
@@ -1170,7 +1170,7 @@ ${plistArray(config.appGroups)}
1170
1170
  }
1171
1171
  if (config.enablePushNotifications) {
1172
1172
  entries.push(` <key>aps-environment</key>
1173
- <string>development</string>`);
1173
+ <string>$(CRAFT_APNS_ENVIRONMENT)</string>`);
1174
1174
  }
1175
1175
  return `<?xml version="1.0" encoding="UTF-8"?>
1176
1176
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -1258,7 +1258,7 @@ function renderAssetCatalog(output, config) {
1258
1258
  const launchBackground = join2(catalog, "LaunchBackground.colorset");
1259
1259
  mkdirSync2(appIcon, { recursive: true });
1260
1260
  mkdirSync2(launchBackground, { recursive: true });
1261
- writeFileSync2(join2(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
1261
+ writeFileSync3(join2(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
1262
1262
  `);
1263
1263
  const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
1264
1264
  if (config.appIconPath) {
@@ -1266,7 +1266,7 @@ function renderAssetCatalog(output, config) {
1266
1266
  throw new Error(`App icon not found: ${config.appIconPath}`);
1267
1267
  cpSync2(config.appIconPath, join2(appIcon, iconFilename));
1268
1268
  }
1269
- writeFileSync2(join2(appIcon, "Contents.json"), `${JSON.stringify({
1269
+ writeFileSync3(join2(appIcon, "Contents.json"), `${JSON.stringify({
1270
1270
  images: iconFilename ? [{
1271
1271
  filename: iconFilename,
1272
1272
  idiom: "universal",
@@ -1279,7 +1279,7 @@ function renderAssetCatalog(output, config) {
1279
1279
  const color = config.backgroundColor?.replace(/^#/, "") || "000000";
1280
1280
  const normalized = color.length === 3 ? [...color].map((value) => `${value}${value}`).join("") : color.padEnd(6, "0").slice(0, 6);
1281
1281
  const components = [0, 2, 4].map((index) => (Number.parseInt(normalized.slice(index, index + 2), 16) / 255).toFixed(3));
1282
- writeFileSync2(join2(launchBackground, "Contents.json"), `${JSON.stringify({
1282
+ writeFileSync3(join2(launchBackground, "Contents.json"), `${JSON.stringify({
1283
1283
  colors: [{
1284
1284
  color: {
1285
1285
  "color-space": "srgb",
@@ -1364,7 +1364,7 @@ async function init(options) {
1364
1364
  `);
1365
1365
  const dirs = [output, join2(output, "Sources"), join2(output, "Shared"), join2(output, "dist")];
1366
1366
  if (options.config?.enableWatchApp)
1367
- dirs.push(join2(output, "WatchApp"));
1367
+ dirs.push(join2(output, "WatchApp"), join2(output, "WatchExtension"));
1368
1368
  for (const dir of dirs) {
1369
1369
  if (!existsSync4(dir)) {
1370
1370
  mkdirSync2(dir, { recursive: true });
@@ -1381,17 +1381,17 @@ async function init(options) {
1381
1381
  };
1382
1382
  if (config.enableBackgroundLocation)
1383
1383
  config.enableGeolocation = true;
1384
- writeFileSync2(join2(output, "craft.config.json"), JSON.stringify(config, null, 2));
1385
- const swiftTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
1384
+ writeFileSync3(join2(output, "craft.config.json"), JSON.stringify(config, null, 2));
1385
+ const swiftTemplate = readFileSync3(join2(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
1386
1386
  const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1387
- writeFileSync2(join2(output, "Sources", `${name}App.swift`), swiftSource);
1388
- const infoPlistTemplate = readFileSync2(join2(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
1387
+ writeFileSync3(join2(output, "Sources", `${name}App.swift`), swiftSource);
1388
+ const infoPlistTemplate = readFileSync3(join2(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
1389
1389
  const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, config.darkMode ? "Dark" : "Light").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>
1390
1390
  <true/>
1391
1391
  <key>NSSupportsLiveActivitiesFrequentUpdates</key>
1392
1392
  <true/>` : "");
1393
- writeFileSync2(join2(output, "Info.plist"), infoPlist);
1394
- const projectYmlTemplate = readFileSync2(join2(TEMPLATES_DIR, "project.yml.template"), "utf-8");
1393
+ writeFileSync3(join2(output, "Info.plist"), infoPlist);
1394
+ const projectYmlTemplate = readFileSync3(join2(TEMPLATES_DIR, "project.yml.template"), "utf-8");
1395
1395
  const nativeDependencies = [];
1396
1396
  if (config.enableLiveActivities)
1397
1397
  nativeDependencies.push(` - target: ${name}LiveActivity`);
@@ -1416,7 +1416,7 @@ async function init(options) {
1416
1416
  }
1417
1417
  if (config.enableWatchApp) {
1418
1418
  nativeTargets.push(` ${name}Watch:
1419
- type: application
1419
+ type: application.watchapp2
1420
1420
  platform: watchOS
1421
1421
  deploymentTarget: "${config.watchosVersion || "9.0"}"
1422
1422
  sources:
@@ -1426,6 +1426,21 @@ async function init(options) {
1426
1426
  CODE_SIGN_ENTITLEMENTS: WatchApp/Watch.entitlements
1427
1427
  PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.watchkitapp
1428
1428
  SWIFT_VERSION: "5.0"
1429
+ SKIP_INSTALL: YES
1430
+ dependencies:
1431
+ - target: ${name}WatchExtension
1432
+ ${name}WatchExtension:
1433
+ type: watchkit2-extension
1434
+ platform: watchOS
1435
+ deploymentTarget: "${config.watchosVersion || "9.0"}"
1436
+ sources:
1437
+ - WatchExtension
1438
+ settings:
1439
+ INFOPLIST_FILE: WatchExtension/Info.plist
1440
+ CODE_SIGN_ENTITLEMENTS: WatchExtension/Watch.entitlements
1441
+ PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.watchkitapp.watchkitextension
1442
+ SWIFT_VERSION: "5.0"
1443
+ APPLICATION_EXTENSION_API_ONLY: YES
1429
1444
  SKIP_INSTALL: YES`);
1430
1445
  }
1431
1446
  const runtimeDir = resolveRuntimeDir(options.runtimeDir);
@@ -1439,24 +1454,27 @@ async function init(options) {
1439
1454
  ${nativeDependencies.join(`
1440
1455
  `)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
1441
1456
  `));
1442
- writeFileSync2(join2(output, "project.yml"), projectYml);
1443
- writeFileSync2(join2(output, "Craft.entitlements"), renderEntitlements(config));
1444
- writeFileSync2(join2(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
1457
+ writeFileSync3(join2(output, "project.yml"), projectYml);
1458
+ writeFileSync3(join2(output, "Craft.entitlements"), renderEntitlements(config));
1459
+ writeFileSync3(join2(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
1445
1460
  renderAssetCatalog(output, config);
1446
1461
  cpSync2(join2(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join2(output, "Shared", "CraftActivityAttributes.swift"));
1447
1462
  if (config.enableLiveActivities) {
1448
1463
  mkdirSync2(join2(output, "WidgetExtension"), { recursive: true });
1449
- const widgetSource = readFileSync2(join2(TEMPLATES_DIR, "CraftLiveActivityWidget.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
1450
- writeFileSync2(join2(output, "WidgetExtension", `${name}LiveActivity.swift`), widgetSource);
1451
- const widgetInfo = readFileSync2(join2(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
1452
- writeFileSync2(join2(output, "WidgetExtension", "Info.plist"), widgetInfo);
1464
+ const widgetSource = readFileSync3(join2(TEMPLATES_DIR, "CraftLiveActivityWidget.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
1465
+ writeFileSync3(join2(output, "WidgetExtension", `${name}LiveActivity.swift`), widgetSource);
1466
+ const widgetInfo = readFileSync3(join2(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
1467
+ writeFileSync3(join2(output, "WidgetExtension", "Info.plist"), widgetInfo);
1453
1468
  }
1454
1469
  if (config.enableWatchApp) {
1455
- const watchSource = readFileSync2(join2(TEMPLATES_DIR, "CraftWatchApp.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
1456
- writeFileSync2(join2(output, "WatchApp", `${name}WatchApp.swift`), watchSource);
1457
- const watchInfo = readFileSync2(join2(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1458
- writeFileSync2(join2(output, "WatchApp", "Info.plist"), watchInfo);
1459
- writeFileSync2(join2(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
1470
+ const watchSource = readFileSync3(join2(TEMPLATES_DIR, "CraftWatchApp.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
1471
+ writeFileSync3(join2(output, "WatchExtension", `${name}WatchApp.swift`), watchSource);
1472
+ const watchInfo = readFileSync3(join2(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1473
+ writeFileSync3(join2(output, "WatchApp", "Info.plist"), watchInfo);
1474
+ writeFileSync3(join2(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
1475
+ const watchExtensionInfo = readFileSync3(join2(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1476
+ writeFileSync3(join2(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
1477
+ writeFileSync3(join2(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
1460
1478
  }
1461
1479
  const placeholderHtml = `<!DOCTYPE html>
1462
1480
  <html>
@@ -1496,7 +1514,7 @@ ${nativeDependencies.join(`
1496
1514
  </script>
1497
1515
  </body>
1498
1516
  </html>`;
1499
- writeFileSync2(join2(output, "dist", "index.html"), placeholderHtml);
1517
+ writeFileSync3(join2(output, "dist", "index.html"), placeholderHtml);
1500
1518
  console.log("\u2705 Project initialized");
1501
1519
  console.log("");
1502
1520
  console.log("Next steps:");
@@ -1514,12 +1532,12 @@ async function build(options) {
1514
1532
  if (!existsSync4(configPath)) {
1515
1533
  throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
1516
1534
  }
1517
- const config = JSON.parse(readFileSync2(configPath, "utf-8"));
1535
+ const config = JSON.parse(readFileSync3(configPath, "utf-8"));
1518
1536
  if (devServer) {
1519
1537
  config.devServerURL = devServer;
1520
1538
  const origin = new URL(devServer).origin;
1521
1539
  config.trustedOrigins = [...new Set([...config.trustedOrigins ?? [], origin])];
1522
- writeFileSync2(configPath, JSON.stringify(config, null, 2));
1540
+ writeFileSync3(configPath, JSON.stringify(config, null, 2));
1523
1541
  console.log(` Dev server: ${devServer}`);
1524
1542
  }
1525
1543
  if (htmlPath) {
@@ -1557,7 +1575,7 @@ async function open(options) {
1557
1575
  await $`open ${projectPath}`;
1558
1576
  }
1559
1577
  function orderSimulators(devices) {
1560
- return [...devices].sort((left, right) => {
1578
+ return devices.filter((device) => device.runtime.startsWith("iOS")).sort((left, right) => {
1561
1579
  const booted = Number(right.state === "Booted") - Number(left.state === "Booted");
1562
1580
  if (booted !== 0)
1563
1581
  return booted;
@@ -1614,7 +1632,7 @@ async function run(options) {
1614
1632
  const configPath = join2(output, "craft.config.json");
1615
1633
  if (!existsSync4(configPath))
1616
1634
  throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
1617
- const config = JSON.parse(readFileSync2(configPath, "utf-8"));
1635
+ const config = JSON.parse(readFileSync3(configPath, "utf-8"));
1618
1636
  if (simulator) {
1619
1637
  console.log("\uD83D\uDCF1 Building and running on simulator...");
1620
1638
  const device = await pickSimulator();
@@ -1712,31 +1730,157 @@ var exports_src2 = {};
1712
1730
  __export(exports_src2, {
1713
1731
  syncAndroidWebAssets: () => syncAndroidWebAssets,
1714
1732
  run: () => run2,
1733
+ resolveRuntimeDir: () => resolveRuntimeDir2,
1715
1734
  renderAndroidPermissions: () => renderAndroidPermissions,
1716
1735
  renderAndroidDeepLinks: () => renderAndroidDeepLinks,
1717
1736
  open: () => open2,
1737
+ installRuntime: () => installRuntime2,
1718
1738
  init: () => init2,
1719
1739
  build: () => build2
1720
1740
  });
1721
- import { cpSync as cpSync3, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
1722
- import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
1741
+ import { cpSync as cpSync3, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
1742
+ import { dirname as dirname2, extname, isAbsolute, join as join3, relative, resolve as resolve2, sep } from "path";
1743
+ function renderAndroidPromiseRuntime(indent = "") {
1744
+ return ANDROID_PROMISE_RUNTIME.trim().split(`
1745
+ `).map((line) => `${indent}${line}`).join(`
1746
+ `);
1747
+ }
1748
+ function escapeXml(value) {
1749
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
1750
+ }
1751
+ function escapeKotlinString(value) {
1752
+ return value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("$", "\\$").replaceAll("\r", "\\r").replaceAll(`
1753
+ `, "\\n");
1754
+ }
1755
+ function generatedPackageSegment(name) {
1756
+ const normalized = name.toLowerCase().replace(/[^a-z0-9_]/g, "");
1757
+ if (!normalized)
1758
+ return "app";
1759
+ return /^[a-z_]/.test(normalized) ? normalized : `app${normalized}`;
1760
+ }
1761
+ function generatedGradleProjectName(name) {
1762
+ const invalidCharacters = ["/", "\\", ":", "<", ">", '"', "?", "*", "|"];
1763
+ const normalized = invalidCharacters.reduce((value, character) => value.replaceAll(character, "-"), name).trim();
1764
+ return normalized || "craft-app";
1765
+ }
1766
+ function requireRegularFile(path2, label) {
1767
+ if (!existsSync5(path2))
1768
+ throw new Error(`${label} not found: ${path2}`);
1769
+ if (!statSync2(path2).isFile())
1770
+ throw new Error(`${label} must be a file: ${path2}`);
1771
+ }
1772
+ function containsPath(parent, candidate) {
1773
+ const relativePath = relative(parent, candidate);
1774
+ return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
1775
+ }
1776
+ function validateGoogleServicesFile(path2, packageName) {
1777
+ requireRegularFile(path2, "Google services file");
1778
+ let document;
1779
+ try {
1780
+ document = JSON.parse(readFileSync4(path2, "utf8"));
1781
+ } catch (error) {
1782
+ throw new Error(`Google services file must contain valid JSON: ${path2}`, { cause: error });
1783
+ }
1784
+ const matchingClient = document.client?.some((client) => {
1785
+ return client.client_info?.android_client_info?.package_name === packageName;
1786
+ });
1787
+ if (!matchingClient) {
1788
+ throw new Error(`Google services file has no client for Android package ${packageName}: ${path2}`);
1789
+ }
1790
+ }
1791
+ function androidWebUrl(value, field) {
1792
+ let url;
1793
+ try {
1794
+ url = new URL(value);
1795
+ } catch {
1796
+ throw new Error(`Invalid ${field}: ${value}`);
1797
+ }
1798
+ const localDevelopment = url.protocol === "http:" && LOCAL_DEVELOPMENT_HOSTS.has(url.hostname);
1799
+ if (url.protocol !== "https:" && !localDevelopment || url.username || url.password) {
1800
+ throw new Error(`${field} must use HTTPS or local HTTP without credentials: ${value}`);
1801
+ }
1802
+ return url;
1803
+ }
1804
+ function normalizeAndroidNetworkConfig(config) {
1805
+ const schemes = config.urlSchemes?.map((value) => value.trim().toLowerCase()) ?? [];
1806
+ if (schemes.some((value) => !/^[a-z][a-z0-9+.-]*$/.test(value))) {
1807
+ throw new Error("Android deep-link schemes must be valid URI schemes");
1808
+ }
1809
+ config.urlSchemes = [...new Set(schemes)];
1810
+ if (config.enableDeepLinks && config.urlSchemes.length === 0) {
1811
+ throw new Error("Android deep links require at least one URL scheme");
1812
+ }
1813
+ const trustedOrigins = (config.trustedOrigins ?? []).map((value) => {
1814
+ return androidWebUrl(value, "Android trusted origin").origin;
1815
+ });
1816
+ if (config.devServerURL) {
1817
+ const devServer = androidWebUrl(config.devServerURL, "Android dev server URL");
1818
+ config.devServerURL = devServer.toString();
1819
+ trustedOrigins.push(devServer.origin);
1820
+ }
1821
+ config.trustedOrigins = [...new Set(trustedOrigins)];
1822
+ }
1823
+ function validateAndroidConfig(config) {
1824
+ if (!config.appName.trim())
1825
+ throw new Error("Android app name must not be empty");
1826
+ const packageSegments = config.packageName.split(".");
1827
+ if (packageSegments.length < 2 || packageSegments.some((segment) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment) || KOTLIN_KEYWORDS.has(segment))) {
1828
+ throw new Error(`Invalid Android package name: ${config.packageName}`);
1829
+ }
1830
+ if (!/^#(?:[\dA-F]{3,4}|[\dA-F]{6}|[\dA-F]{8})$/i.test(config.backgroundColor ?? "")) {
1831
+ throw new Error(`Invalid Android background color: ${config.backgroundColor}`);
1832
+ }
1833
+ for (const [name, value] of [
1834
+ ["versionCode", config.versionCode],
1835
+ ["minSdk", config.minSdk],
1836
+ ["compileSdk", config.compileSdk],
1837
+ ["targetSdk", config.targetSdk]
1838
+ ]) {
1839
+ if (!Number.isInteger(value) || Number(value) < 1) {
1840
+ throw new Error(`Android ${name} must be a positive integer`);
1841
+ }
1842
+ }
1843
+ if (Number(config.minSdk) > Number(config.targetSdk)) {
1844
+ throw new Error("Android minSdk must not exceed targetSdk");
1845
+ }
1846
+ if (Number(config.targetSdk) > Number(config.compileSdk)) {
1847
+ throw new Error("Android targetSdk must not exceed compileSdk");
1848
+ }
1849
+ }
1850
+ function writeAndroidConfig(output, config) {
1851
+ writeFileSync4(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
1852
+ const runtimeConfig = { ...config };
1853
+ delete runtimeConfig.appIconPath;
1854
+ delete runtimeConfig.googleServicesFile;
1855
+ writeFileSync4(join3(output, "app/src/main/assets/craft.config.json"), JSON.stringify(runtimeConfig, null, 2));
1856
+ }
1723
1857
  function syncAndroidWebAssets(source, output) {
1724
1858
  const sourcePath = resolve2(source);
1725
1859
  if (!existsSync5(sourcePath))
1726
1860
  throw new Error(`Web asset path not found: ${source}`);
1727
- const assetsDir = join3(output, "app/src/main/assets");
1861
+ const sourceStat = statSync2(sourcePath);
1862
+ if (!sourceStat.isDirectory() && !sourceStat.isFile()) {
1863
+ throw new Error(`Web asset path must be a file or directory: ${source}`);
1864
+ }
1865
+ const assetsDir = resolve2(output, "app/src/main/assets");
1866
+ if (containsPath(sourcePath, assetsDir) || containsPath(assetsDir, sourcePath)) {
1867
+ throw new Error(`Web asset source must not overlap generated asset directory: ${source}`);
1868
+ }
1869
+ if (sourceStat.isDirectory()) {
1870
+ requireRegularFile(join3(sourcePath, "index.html"), "Web asset directory entry point");
1871
+ }
1728
1872
  const configPath = join3(assetsDir, "craft.config.json");
1729
- const config = existsSync5(configPath) ? readFileSync3(configPath) : undefined;
1873
+ const config = existsSync5(configPath) ? readFileSync4(configPath) : undefined;
1730
1874
  rmSync3(assetsDir, { recursive: true, force: true });
1731
1875
  mkdirSync3(assetsDir, { recursive: true });
1732
- if (statSync2(sourcePath).isDirectory())
1876
+ if (sourceStat.isDirectory())
1733
1877
  cpSync3(sourcePath, assetsDir, { recursive: true });
1734
1878
  else
1735
1879
  cpSync3(sourcePath, join3(assetsDir, "index.html"));
1736
1880
  if (!existsSync5(join3(assetsDir, "index.html")))
1737
1881
  throw new Error(`Web asset directory must contain index.html: ${source}`);
1738
1882
  if (config)
1739
- writeFileSync3(configPath, config);
1883
+ writeFileSync4(configPath, config);
1740
1884
  }
1741
1885
  function renderAndroidPermissions(config) {
1742
1886
  const permissions = new Set(["android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE"]);
@@ -1786,14 +1930,69 @@ function renderAndroidDeepLinks(config) {
1786
1930
  </intent-filter>`).join(`
1787
1931
  `);
1788
1932
  }
1933
+ function resolveRuntimeDir2(override) {
1934
+ if (override === null)
1935
+ return null;
1936
+ const dir = override ?? process.env.CRAFT_ANDROID_RUNTIME;
1937
+ if (!dir)
1938
+ return null;
1939
+ if (!existsSync5(dir)) {
1940
+ const source = override === undefined ? "CRAFT_ANDROID_RUNTIME points at" : "runtimeDir is";
1941
+ throw new Error(`${source} ${dir}, which does not exist.`);
1942
+ }
1943
+ return dir;
1944
+ }
1945
+ function installRuntime2(output, runtimeDir) {
1946
+ const found = RUNTIME_ABIS.map((abi) => ({ abi, source: join3(runtimeDir, abi, "libcraft.so") })).filter((entry) => existsSync5(entry.source));
1947
+ if (found.length === 0) {
1948
+ 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.");
1949
+ }
1950
+ if (found.length < RUNTIME_ABIS.length) {
1951
+ const missing = RUNTIME_ABIS.filter((abi) => !found.some((entry) => entry.abi === abi));
1952
+ 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.");
1953
+ }
1954
+ const dest = join3(output, "app/src/main/jniLibs");
1955
+ rmSync3(dest, { force: true, recursive: true });
1956
+ for (const { abi, source } of found) {
1957
+ const abiDir = join3(dest, abi);
1958
+ mkdirSync3(abiDir, { recursive: true });
1959
+ cpSync3(source, join3(abiDir, "libcraft.so"));
1960
+ }
1961
+ return true;
1962
+ }
1789
1963
  async function init2(options) {
1790
1964
  const { name, packageName, output } = options;
1791
1965
  console.log(`
1792
1966
  \u26A1 Initializing Craft Android project: ${name}`);
1793
1967
  console.log(` Output: ${output}
1794
1968
  `);
1795
- const finalPackageName = packageName || `com.craft.${name.toLowerCase().replace(/[^a-z0-9]/g, "")}`;
1969
+ const finalPackageName = packageName || `com.craft.${generatedPackageSegment(name)}`;
1796
1970
  const packagePath = finalPackageName.replace(/\./g, "/");
1971
+ const config = {
1972
+ ...DEFAULT_CONFIG2,
1973
+ ...options.config,
1974
+ appName: name,
1975
+ packageName: finalPackageName
1976
+ };
1977
+ if (config.enableBackgroundLocation)
1978
+ config.enableGeolocation = true;
1979
+ if (config.enableHealthConnect) {
1980
+ config.minSdk = Math.max(config.minSdk ?? 26, 26);
1981
+ config.compileSdk = Math.max(config.compileSdk ?? 36, 36);
1982
+ }
1983
+ normalizeAndroidNetworkConfig(config);
1984
+ validateAndroidConfig(config);
1985
+ if (config.enablePushNotifications && !config.googleServicesFile) {
1986
+ throw new Error("Android push notifications require a googleServicesFile");
1987
+ }
1988
+ if (config.googleServicesFile)
1989
+ validateGoogleServicesFile(config.googleServicesFile, finalPackageName);
1990
+ if (config.appIconPath)
1991
+ requireRegularFile(config.appIconPath, "App icon");
1992
+ const appIconExtension = config.appIconPath ? extname(config.appIconPath).toLowerCase() : undefined;
1993
+ if (config.appIconPath && ![".gif", ".jpg", ".png", ".webp"].includes(appIconExtension ?? "")) {
1994
+ throw new Error(`Unsupported Android app icon format: ${appIconExtension || "(none)"}`);
1995
+ }
1797
1996
  const dirs = [
1798
1997
  output,
1799
1998
  join3(output, "app/src/main/java", packagePath),
@@ -1803,32 +2002,24 @@ async function init2(options) {
1803
2002
  join3(output, "app/src/main/assets"),
1804
2003
  join3(output, "gradle/wrapper")
1805
2004
  ];
2005
+ if (existsSync5(output) && !statSync2(output).isDirectory()) {
2006
+ throw new Error(`Android project output must be a directory: ${output}`);
2007
+ }
1806
2008
  for (const dir of dirs) {
1807
2009
  if (!existsSync5(dir)) {
1808
2010
  mkdirSync3(dir, { recursive: true });
1809
2011
  }
1810
2012
  }
1811
- const config = {
1812
- ...DEFAULT_CONFIG2,
1813
- appName: name,
1814
- packageName: finalPackageName,
1815
- ...options.config
1816
- };
1817
- if (config.enableBackgroundLocation)
1818
- config.enableGeolocation = true;
1819
- writeFileSync3(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
1820
- writeFileSync3(join3(output, "app/src/main/assets/craft.config.json"), JSON.stringify(config, null, 2));
2013
+ writeAndroidConfig(output, config);
1821
2014
  const hasGoogleServices = Boolean(config.googleServicesFile);
1822
2015
  if (config.googleServicesFile) {
1823
- if (!existsSync5(config.googleServicesFile))
1824
- throw new Error(`Google services file not found: ${config.googleServicesFile}`);
1825
2016
  cpSync3(config.googleServicesFile, join3(output, "app/google-services.json"));
1826
2017
  }
1827
- const mainActivityTemplate = readFileSync3(join3(TEMPLATES_DIR2, "MainActivity.kt.template"), "utf-8");
2018
+ const mainActivityTemplate = readFileSync4(join3(TEMPLATES_DIR2, "MainActivity.kt.template"), "utf-8");
1828
2019
  const mainActivity = mainActivityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
1829
- writeFileSync3(join3(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
1830
- const craftBridgeTemplate = readFileSync3(join3(TEMPLATES_DIR2, "CraftBridge.kt.template"), "utf-8");
1831
- const craftBridge = craftBridgeTemplate.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 {
2020
+ writeFileSync4(join3(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
2021
+ const craftBridgeTemplate = readFileSync4(join3(TEMPLATES_DIR2, "CraftBridge.kt.template"), "utf-8");
2022
+ 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 {
1832
2023
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
1833
2024
  && ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
1834
2025
  ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 4204)
@@ -1838,27 +2029,26 @@ async function init2(options) {
1838
2029
  val token = if (task.isSuccessful) task.result else null
1839
2030
  val callback = if (token.isNullOrBlank()) "window._craftPushReject" else "window._craftPushResolve"
1840
2031
  val payload = JSONObject.quote(token ?: task.exception?.message ?: "Firebase Cloud Messaging is not configured")
1841
- webView.evaluateJavascript("$callback && $callback($payload)", null)
2032
+ evaluatePromiseJavascript("$callback && $callback($payload)")
1842
2033
  }
1843
2034
  } catch (error: Exception) {
1844
2035
  val payload = JSONObject.quote(error.message ?: "Firebase Cloud Messaging is not configured")
1845
- webView.evaluateJavascript("window._craftPushReject && window._craftPushReject($payload)", null)
2036
+ evaluatePromiseJavascript("window._craftPushReject && window._craftPushReject($payload)")
1846
2037
  }
1847
- }` : `activity.runOnUiThread {
1848
- webView.evaluateJavascript(
1849
- "window._craftPushReject && window._craftPushReject('Push notifications are disabled')",
1850
- null
1851
- )
1852
- }`);
1853
- writeFileSync3(join3(output, "app/src/main/java", packagePath, "CraftBridge.kt"), craftBridge);
1854
- const healthTemplate = readFileSync3(join3(TEMPLATES_DIR2, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
1855
- writeFileSync3(join3(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
1856
- if (config.enableBackgroundLocation) {
1857
- const serviceTemplate = readFileSync3(join3(TEMPLATES_DIR2, "LocationRecordingService.kt.template"), "utf-8");
1858
- writeFileSync3(join3(output, "app/src/main/java", packagePath, "LocationRecordingService.kt"), serviceTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
1859
- }
1860
- const manifestTemplate = readFileSync3(join3(TEMPLATES_DIR2, "AndroidManifest.xml.template"), "utf-8");
1861
- 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=".LocationRecordingService" android:exported="false" android:foregroundServiceType="location" android:stopWithTask="false" />' : "").replace(/\{\{HEALTH_CONNECT_QUERIES\}\}/g, config.enableHealthConnect ? ` <queries>
2038
+ }` : `evaluatePromiseJavascript(
2039
+ "window._craftPushReject && window._craftPushReject('Push notifications are disabled')"
2040
+ )`);
2041
+ writeFileSync4(join3(output, "app/src/main/java", packagePath, "CraftBridge.kt"), craftBridge);
2042
+ const craftNative = readFileSync4(join3(TEMPLATES_DIR2, "CraftNative.kt.template"), "utf-8");
2043
+ const nativeDir = join3(output, "app/src/main/java/com/craft/runtime");
2044
+ mkdirSync3(nativeDir, { recursive: true });
2045
+ writeFileSync4(join3(nativeDir, "CraftNative.kt"), craftNative);
2046
+ const serviceTemplate = readFileSync4(join3(TEMPLATES_DIR2, "LocationRecordingService.kt.template"), "utf-8");
2047
+ writeFileSync4(join3(nativeDir, "LocationRecordingService.kt"), serviceTemplate);
2048
+ const healthTemplate = readFileSync4(join3(TEMPLATES_DIR2, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
2049
+ writeFileSync4(join3(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
2050
+ const manifestTemplate = readFileSync4(join3(TEMPLATES_DIR2, "AndroidManifest.xml.template"), "utf-8");
2051
+ 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>
1862
2052
  <package android:name="com.google.android.apps.healthdata" />
1863
2053
  </queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
1864
2054
  <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
@@ -1867,25 +2057,25 @@ async function init2(options) {
1867
2057
  <action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
1868
2058
  <category android:name="android.intent.category.HEALTH_PERMISSIONS" />
1869
2059
  </intent-filter>` : "");
1870
- writeFileSync3(join3(output, "app/src/main/AndroidManifest.xml"), manifest);
1871
- const projectGradleTemplate = readFileSync3(join3(TEMPLATES_DIR2, "build.gradle.kts.project.template"), "utf-8");
1872
- writeFileSync3(join3(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
1873
- const appGradleTemplate = readFileSync3(join3(TEMPLATES_DIR2, "build.gradle.kts.app.template"), "utf-8");
1874
- const appGradle = appGradleTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{VERSION_NAME\}\}/g, config.version || "1.0.0").replace(/\{\{VERSION_CODE\}\}/g, String(config.versionCode || 1)).replace(/\{\{MIN_SDK\}\}/g, String(config.minSdk || 24)).replace(/\{\{COMPILE_SDK\}\}/g, String(Math.max(config.compileSdk || 36, config.enableHealthConnect ? 36 : 1))).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")
2060
+ writeFileSync4(join3(output, "app/src/main/AndroidManifest.xml"), manifest);
2061
+ const projectGradleTemplate = readFileSync4(join3(TEMPLATES_DIR2, "build.gradle.kts.project.template"), "utf-8");
2062
+ 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' : ""));
2063
+ const appGradleTemplate = readFileSync4(join3(TEMPLATES_DIR2, "build.gradle.kts.app.template"), "utf-8");
2064
+ 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 || 24)).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")
1875
2065
  implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")` : "");
1876
- writeFileSync3(join3(output, "app/build.gradle.kts"), appGradle);
1877
- const proguardTemplate = readFileSync3(join3(TEMPLATES_DIR2, "proguard-rules.pro.template"), "utf-8");
1878
- writeFileSync3(join3(output, "app/proguard-rules.pro"), proguardTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
1879
- const settingsTemplate = readFileSync3(join3(TEMPLATES_DIR2, "settings.gradle.kts.template"), "utf-8");
1880
- const settings2 = settingsTemplate.replace(/\{\{APP_NAME\}\}/g, name);
1881
- writeFileSync3(join3(output, "settings.gradle.kts"), settings2);
2066
+ writeFileSync4(join3(output, "app/build.gradle.kts"), appGradle);
2067
+ const proguardTemplate = readFileSync4(join3(TEMPLATES_DIR2, "proguard-rules.pro.template"), "utf-8");
2068
+ writeFileSync4(join3(output, "app/proguard-rules.pro"), proguardTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
2069
+ const settingsTemplate = readFileSync4(join3(TEMPLATES_DIR2, "settings.gradle.kts.template"), "utf-8");
2070
+ const settings2 = settingsTemplate.replace(/\{\{APP_NAME\}\}/g, escapeKotlinString(generatedGradleProjectName(name)));
2071
+ writeFileSync4(join3(output, "settings.gradle.kts"), settings2);
1882
2072
  const gradleProperties = `org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
1883
2073
  android.useAndroidX=true
1884
2074
  kotlin.code.style=official
1885
2075
  android.nonTransitiveRClass=true
1886
2076
  `;
1887
- writeFileSync3(join3(output, "gradle.properties"), gradleProperties);
1888
- writeFileSync3(join3(output, "local.properties"), `# SDK location will be set by Android Studio
2077
+ writeFileSync4(join3(output, "gradle.properties"), gradleProperties);
2078
+ writeFileSync4(join3(output, "local.properties"), `# SDK location will be set by Android Studio
1889
2079
  `);
1890
2080
  const gradleWrapperProps = `distributionBase=GRADLE_USER_HOME
1891
2081
  distributionPath=wrapper/dists
@@ -1895,13 +2085,13 @@ validateDistributionUrl=true
1895
2085
  zipStoreBase=GRADLE_USER_HOME
1896
2086
  zipStorePath=wrapper/dists
1897
2087
  `;
1898
- writeFileSync3(join3(output, "gradle/wrapper/gradle-wrapper.properties"), gradleWrapperProps);
2088
+ writeFileSync4(join3(output, "gradle/wrapper/gradle-wrapper.properties"), gradleWrapperProps);
1899
2089
  const stringsXml = `<?xml version="1.0" encoding="utf-8"?>
1900
2090
  <resources>
1901
- <string name="app_name">${name}</string>
2091
+ <string name="app_name">${escapeXml(name)}</string>
1902
2092
  </resources>
1903
2093
  `;
1904
- writeFileSync3(join3(output, "app/src/main/res/values/strings.xml"), stringsXml);
2094
+ writeFileSync4(join3(output, "app/src/main/res/values/strings.xml"), stringsXml);
1905
2095
  const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
1906
2096
  <resources>
1907
2097
  <color name="primary">#1a1a2e</color>
@@ -1910,7 +2100,7 @@ zipStorePath=wrapper/dists
1910
2100
  <color name="background">${config.backgroundColor}</color>
1911
2101
  </resources>
1912
2102
  `;
1913
- writeFileSync3(join3(output, "app/src/main/res/values/colors.xml"), colorsXml);
2103
+ writeFileSync4(join3(output, "app/src/main/res/values/colors.xml"), colorsXml);
1914
2104
  const appIconXml = `<?xml version="1.0" encoding="utf-8"?>
1915
2105
  <vector xmlns:android="http://schemas.android.com/apk/res/android"
1916
2106
  android:width="108dp"
@@ -1922,11 +2112,9 @@ zipStorePath=wrapper/dists
1922
2112
  </vector>
1923
2113
  `;
1924
2114
  if (config.appIconPath) {
1925
- if (!existsSync5(config.appIconPath))
1926
- throw new Error(`App icon not found: ${config.appIconPath}`);
1927
- cpSync3(config.appIconPath, join3(output, "app/src/main/res/drawable/craft_app_icon.png"));
2115
+ cpSync3(config.appIconPath, join3(output, `app/src/main/res/drawable/craft_app_icon${appIconExtension}`));
1928
2116
  } else {
1929
- writeFileSync3(join3(output, "app/src/main/res/drawable/craft_app_icon.xml"), appIconXml);
2117
+ writeFileSync4(join3(output, "app/src/main/res/drawable/craft_app_icon.xml"), appIconXml);
1930
2118
  }
1931
2119
  const themesXml = `<?xml version="1.0" encoding="utf-8"?>
1932
2120
  <resources>
@@ -1937,7 +2125,7 @@ zipStorePath=wrapper/dists
1937
2125
  </style>
1938
2126
  </resources>
1939
2127
  `;
1940
- writeFileSync3(join3(output, "app/src/main/res/values/themes.xml"), themesXml);
2128
+ writeFileSync4(join3(output, "app/src/main/res/values/themes.xml"), themesXml);
1941
2129
  const activityMainXml = `<?xml version="1.0" encoding="utf-8"?>
1942
2130
  <androidx.coordinatorlayout.widget.CoordinatorLayout
1943
2131
  xmlns:android="http://schemas.android.com/apk/res/android"
@@ -1952,13 +2140,13 @@ zipStorePath=wrapper/dists
1952
2140
 
1953
2141
  </androidx.coordinatorlayout.widget.CoordinatorLayout>
1954
2142
  `;
1955
- writeFileSync3(join3(output, "app/src/main/res/layout/activity_main.xml"), activityMainXml);
2143
+ writeFileSync4(join3(output, "app/src/main/res/layout/activity_main.xml"), activityMainXml);
1956
2144
  const placeholderHtml = `<!DOCTYPE html>
1957
2145
  <html>
1958
2146
  <head>
1959
2147
  <meta charset="UTF-8">
1960
2148
  <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
1961
- <title>${name}</title>
2149
+ <title>${escapeXml(name)}</title>
1962
2150
  <style>
1963
2151
  * { margin: 0; padding: 0; box-sizing: border-box; }
1964
2152
  body {
@@ -1978,7 +2166,7 @@ zipStorePath=wrapper/dists
1978
2166
  </head>
1979
2167
  <body>
1980
2168
  <div class="container">
1981
- <h1>\u26A1 ${name}</h1>
2169
+ <h1>\u26A1 ${escapeXml(name)}</h1>
1982
2170
  <p>Built with Craft Android</p>
1983
2171
  <p class="ready" id="status">Waiting for Craft bridge...</p>
1984
2172
  </div>
@@ -1990,7 +2178,12 @@ zipStorePath=wrapper/dists
1990
2178
  </script>
1991
2179
  </body>
1992
2180
  </html>`;
1993
- writeFileSync3(join3(output, "app/src/main/assets/index.html"), placeholderHtml);
2181
+ writeFileSync4(join3(output, "app/src/main/assets/index.html"), placeholderHtml);
2182
+ const runtimeDir = resolveRuntimeDir2(options.runtimeDir);
2183
+ if (runtimeDir) {
2184
+ installRuntime2(output, runtimeDir);
2185
+ console.log(" Installed the Zig runtime from", runtimeDir);
2186
+ }
1994
2187
  console.log("\u2705 Project initialized");
1995
2188
  console.log("");
1996
2189
  console.log("Next steps:");
@@ -2008,19 +2201,30 @@ async function build2(options) {
2008
2201
  if (!existsSync5(configPath)) {
2009
2202
  throw new Error(`No craft.config.json found in ${output}. Run 'craft android init' first.`);
2010
2203
  }
2011
- const config = JSON.parse(readFileSync3(configPath, "utf-8"));
2204
+ const config = JSON.parse(readFileSync4(configPath, "utf-8"));
2205
+ if (existsSync5(join3(output, "app/src/main/jniLibs"))) {
2206
+ const runtimeDir = resolveRuntimeDir2(options.runtimeDir);
2207
+ if (runtimeDir) {
2208
+ installRuntime2(output, runtimeDir);
2209
+ console.log(" Refreshed the Zig runtime from", runtimeDir);
2210
+ } else {
2211
+ console.log(" Keeping the Zig runtime installed at init (no runtime directory configured)");
2212
+ }
2213
+ }
2012
2214
  if (devServer) {
2013
- config.devServerURL = devServer;
2014
- config.trustedOrigins = [...new Set([...config.trustedOrigins ?? [], new URL(devServer).origin])];
2015
- writeFileSync3(configPath, JSON.stringify(config, null, 2));
2016
- writeFileSync3(join3(output, "app/src/main/assets/craft.config.json"), JSON.stringify(config, null, 2));
2215
+ const url = androidWebUrl(devServer, "Android dev server URL");
2216
+ config.devServerURL = url.toString();
2217
+ config.trustedOrigins = [...new Set([
2218
+ ...(config.trustedOrigins ?? []).map((value) => androidWebUrl(value, "Android trusted origin").origin),
2219
+ url.origin
2220
+ ])];
2221
+ writeAndroidConfig(output, config);
2017
2222
  console.log(` Dev server: ${devServer}`);
2018
2223
  }
2019
2224
  if (htmlPath) {
2020
2225
  syncAndroidWebAssets(htmlPath, output);
2021
2226
  config.hasBundledFallback = Boolean(devServer);
2022
- writeFileSync3(configPath, JSON.stringify(config, null, 2));
2023
- writeFileSync3(join3(output, "app/src/main/assets/craft.config.json"), JSON.stringify(config, null, 2));
2227
+ writeAndroidConfig(output, config);
2024
2228
  console.log(` Synced: ${htmlPath} \u2192 assets/`);
2025
2229
  }
2026
2230
  if (!compile)
@@ -2075,7 +2279,7 @@ async function run2(options) {
2075
2279
  } else {
2076
2280
  await $2`adb install -r ${apkPath}`;
2077
2281
  }
2078
- const config = JSON.parse(readFileSync3(join3(output, "craft.config.json"), "utf-8"));
2282
+ const config = JSON.parse(readFileSync4(join3(output, "craft.config.json"), "utf-8"));
2079
2283
  const launchCmd = `${config.packageName}/.MainActivity`;
2080
2284
  if (device) {
2081
2285
  await $2`adb -s ${device} shell am start -n ${launchCmd}`;
@@ -2090,10 +2294,106 @@ async function run2(options) {
2090
2294
  throw error;
2091
2295
  }
2092
2296
  }
2093
- var $2, TEMPLATES_DIR2, DEFAULT_CONFIG2;
2297
+ var $2, ANDROID_PROMISE_RUNTIME = `
2298
+ if (window.__craftRejectPendingPromises) {
2299
+ window.__craftRejectPendingPromises('Android bridge reinitialized');
2300
+ }
2301
+ if (window.__craftRejectPermissionRequests) {
2302
+ window.__craftRejectPermissionRequests('Android bridge reinitialized');
2303
+ }
2304
+ window.__craftPromiseRuntimeClosed = false;
2305
+ window.__craftPendingPromises = Object.create(null);
2306
+ window.__craftPromise = function(channel, resolveName, rejectName, invoke, timeoutMs, timeoutError) {
2307
+ if (window.__craftPromiseRuntimeClosed) {
2308
+ return Promise.reject(new Error('Android bridge is closed'));
2309
+ }
2310
+ if (window.__craftPendingPromises[channel]) {
2311
+ return Promise.reject(new Error('A '.concat(channel, ' request is already in progress')));
2312
+ }
2313
+
2314
+ return new Promise(function(resolve, reject) {
2315
+ var entry = {settled: false, timer: null, settle: null};
2316
+ var resolveCallback;
2317
+ var rejectCallback;
2318
+
2319
+ entry.settle = function(succeeded, value) {
2320
+ if (entry.settled || window.__craftPendingPromises[channel] !== entry) return;
2321
+ entry.settled = true;
2322
+ if (entry.timer !== null) clearTimeout(entry.timer);
2323
+ if (window[resolveName] === resolveCallback) window[resolveName] = null;
2324
+ if (window[rejectName] === rejectCallback) window[rejectName] = null;
2325
+ delete window.__craftPendingPromises[channel];
2326
+ if (succeeded) resolve(value);
2327
+ else reject(value);
2328
+ };
2329
+
2330
+ resolveCallback = function(value) {
2331
+ entry.settle(true, value);
2332
+ };
2333
+ rejectCallback = function(error) {
2334
+ entry.settle(false, error);
2335
+ };
2336
+ window[resolveName] = resolveCallback;
2337
+ window[rejectName] = rejectCallback;
2338
+ window.__craftPendingPromises[channel] = entry;
2339
+
2340
+ if (timeoutMs > 0) {
2341
+ entry.timer = setTimeout(function() {
2342
+ entry.settle(false, timeoutError || new Error(channel.concat(' request timed out')));
2343
+ }, timeoutMs);
2344
+ }
2345
+
2346
+ try {
2347
+ invoke();
2348
+ }
2349
+ catch (error) {
2350
+ entry.settle(false, error);
2351
+ }
2352
+ });
2353
+ };
2354
+
2355
+ window.__craftRejectPendingPromises = function(message) {
2356
+ window.__craftPromiseRuntimeClosed = true;
2357
+ Object.keys(window.__craftPendingPromises).forEach(function(channel) {
2358
+ var entry = window.__craftPendingPromises[channel];
2359
+ if (entry) entry.settle(false, new Error(message || 'Android bridge closed'));
2360
+ });
2361
+ };
2362
+ `, TEMPLATES_DIR2, LOCAL_DEVELOPMENT_HOSTS, KOTLIN_KEYWORDS, DEFAULT_CONFIG2, RUNTIME_ABIS;
2094
2363
  var init_src2 = __esm(() => {
2095
2364
  ({ $: $2 } = globalThis.Bun);
2096
2365
  TEMPLATES_DIR2 = join3(dirname2(import.meta.dir), "templates");
2366
+ LOCAL_DEVELOPMENT_HOSTS = new Set(["localhost", "127.0.0.1", "10.0.2.2"]);
2367
+ KOTLIN_KEYWORDS = new Set([
2368
+ "as",
2369
+ "break",
2370
+ "class",
2371
+ "continue",
2372
+ "do",
2373
+ "else",
2374
+ "false",
2375
+ "for",
2376
+ "fun",
2377
+ "if",
2378
+ "in",
2379
+ "interface",
2380
+ "is",
2381
+ "null",
2382
+ "object",
2383
+ "package",
2384
+ "return",
2385
+ "super",
2386
+ "this",
2387
+ "throw",
2388
+ "true",
2389
+ "try",
2390
+ "typealias",
2391
+ "typeof",
2392
+ "val",
2393
+ "var",
2394
+ "when",
2395
+ "while"
2396
+ ]);
2097
2397
  DEFAULT_CONFIG2 = {
2098
2398
  version: "1.0.0",
2099
2399
  versionCode: 1,
@@ -2117,6 +2417,7 @@ var init_src2 = __esm(() => {
2117
2417
  compileSdk: 36,
2118
2418
  targetSdk: 35
2119
2419
  };
2420
+ RUNTIME_ABIS = ["arm64-v8a", "x86_64"];
2120
2421
  });
2121
2422
 
2122
2423
  // ../../node_modules/@stacksjs/clapp/dist/index.js
@@ -4262,8 +4563,28 @@ function craftBinaryNotFoundMessage(triedPath) {
4262
4563
  ].join(`
4263
4564
  `);
4264
4565
  }
4566
+
4567
+ // src/scaffold-version.ts
4568
+ import { readFileSync, writeFileSync } from "fs";
4569
+ function pinCraftNativeDependency(packagePath, craftVersion) {
4570
+ const manifest = JSON.parse(readFileSync(packagePath, "utf-8"));
4571
+ const sections = [manifest.dependencies, manifest.devDependencies];
4572
+ let found = false;
4573
+ for (const dependencies of sections) {
4574
+ if (dependencies && Object.hasOwn(dependencies, "craft-native")) {
4575
+ dependencies["craft-native"] = `^${craftVersion}`;
4576
+ found = true;
4577
+ }
4578
+ }
4579
+ if (!found) {
4580
+ manifest.dependencies ??= {};
4581
+ manifest.dependencies["craft-native"] = `^${craftVersion}`;
4582
+ }
4583
+ writeFileSync(packagePath, `${JSON.stringify(manifest, null, 2)}
4584
+ `);
4585
+ }
4265
4586
  // package.json
4266
- var version = "0.0.90";
4587
+ var version = "0.0.92";
4267
4588
 
4268
4589
  // bin/cli.ts
4269
4590
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
@@ -4757,7 +5078,7 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
4757
5078
  const template = options?.template || "blank";
4758
5079
  const bundleId = options?.bundleId || `com.example.${name.toLowerCase().replace(/[^a-z0-9]/g, "")}`;
4759
5080
  const appNameSlug = name.toLowerCase().replace(/[^a-z0-9]/g, "-");
4760
- const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync4, existsSync: existsSync7, readdirSync: readdirSync3, readFileSync: readFileSync4, cpSync: cpSync4 } = await import("fs");
5081
+ const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync5, existsSync: existsSync7, readdirSync: readdirSync3, readFileSync: readFileSync5, cpSync: cpSync4 } = await import("fs");
4761
5082
  const { join: join4, dirname: dirname3 } = await import("path");
4762
5083
  const replaceVars = (content) => {
4763
5084
  return content.replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{APP_NAME_SLUG\}\}/g, appNameSlug).replace(/\{\{BUNDLE_ID\}\}/g, bundleId).replace(/\{\{AUTHOR\}\}/g, "Developer");
@@ -4779,9 +5100,9 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
4779
5100
  if (entry.isDirectory()) {
4780
5101
  copyRecursive(srcPath, destPath);
4781
5102
  } else {
4782
- const content = readFileSync4(srcPath, "utf-8");
5103
+ const content = readFileSync5(srcPath, "utf-8");
4783
5104
  const processedContent = replaceVars(content);
4784
- writeFileSync4(destPath, processedContent);
5105
+ writeFileSync5(destPath, processedContent);
4785
5106
  }
4786
5107
  }
4787
5108
  };
@@ -4796,7 +5117,7 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
4796
5117
  const copied = await copyTemplate(template, name);
4797
5118
  if (!copied) {
4798
5119
  mkdirSync4(join4(name, "src"), { recursive: true });
4799
- writeFileSync4(join4(name, "index.html"), replaceVars(`<!DOCTYPE html>
5120
+ writeFileSync5(join4(name, "index.html"), replaceVars(`<!DOCTYPE html>
4800
5121
  <html lang="en">
4801
5122
  <head>
4802
5123
  <meta charset="UTF-8">
@@ -4810,7 +5131,7 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
4810
5131
  </div>
4811
5132
  </body>
4812
5133
  </html>`));
4813
- writeFileSync4(join4(name, "package.json"), replaceVars(JSON.stringify({
5134
+ writeFileSync5(join4(name, "package.json"), replaceVars(JSON.stringify({
4814
5135
  name: "{{APP_NAME_SLUG}}",
4815
5136
  version: "1.0.0",
4816
5137
  private: true,
@@ -4820,10 +5141,11 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
4820
5141
  build: "craft build"
4821
5142
  },
4822
5143
  dependencies: {
4823
- "craft-native": "workspace:*"
5144
+ "craft-native": `^${version}`
4824
5145
  }
4825
5146
  }, null, 2)));
4826
5147
  }
5148
+ pinCraftNativeDependency(join4(name, "package.json"), version);
4827
5149
  console.log(`\u2705 ${template} project created`);
4828
5150
  }
4829
5151
  if (template === "desktop" || template === "all") {
@@ -4843,7 +5165,7 @@ export default {
4843
5165
  },
4844
5166
  } satisfies CraftConfig
4845
5167
  `;
4846
- writeFileSync4(`${name}/craft.config.ts`, configContent);
5168
+ writeFileSync5(`${name}/craft.config.ts`, configContent);
4847
5169
  const htmlContent = `<!DOCTYPE html>
4848
5170
  <html lang="en">
4849
5171
  <head>
@@ -4874,7 +5196,7 @@ export default {
4874
5196
  </body>
4875
5197
  </html>
4876
5198
  `;
4877
- writeFileSync4(`${name}/index.html`, htmlContent);
5199
+ writeFileSync5(`${name}/index.html`, htmlContent);
4878
5200
  const packageJson = {
4879
5201
  name: appNameSlug,
4880
5202
  version: "0.1.0",
@@ -4887,10 +5209,11 @@ export default {
4887
5209
  "ios:open": "craft ios open"
4888
5210
  },
4889
5211
  devDependencies: {
4890
- "craft-native": "*"
5212
+ "craft-native": `^${version}`
4891
5213
  }
4892
5214
  };
4893
- writeFileSync4(`${name}/package.json`, JSON.stringify(packageJson, null, 2));
5215
+ writeFileSync5(`${name}/package.json`, JSON.stringify(packageJson, null, 2));
5216
+ pinCraftNativeDependency(`${name}/package.json`, version);
4894
5217
  console.log("\u2705 Desktop project created");
4895
5218
  }
4896
5219
  if (template === "ios" || template === "all") {