craft-native 0.0.89 → 0.0.91

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 (29) hide show
  1. package/dist/android/src/index.js +285 -44
  2. package/dist/android/src/promise-runtime.d.ts +3 -0
  3. package/dist/android/templates/CraftBridge.kt.template +1951 -1193
  4. package/dist/android/templates/CraftHealthConnect.kt.template +45 -6
  5. package/dist/android/templates/CraftHealthConnectStub.kt.template +7 -1
  6. package/dist/android/templates/CraftNative.kt.template +2231 -0
  7. package/dist/android/templates/LocationRecordingService.kt.template +34 -9
  8. package/dist/android/templates/MainActivity.kt.template +25 -2
  9. package/dist/android/templates/proguard-rules.pro.template +4 -1
  10. package/dist/android/templates/test-bridges.html +10 -33
  11. package/dist/api/index.d.ts +1 -1
  12. package/dist/api/ios-advanced.d.ts +8 -5
  13. package/dist/api/live-activity-handle.d.ts +6 -0
  14. package/dist/api/mobile.d.ts +13 -5
  15. package/dist/api/window.d.ts +2 -0
  16. package/dist/cli.js +404 -128
  17. package/dist/index.cjs +65 -17
  18. package/dist/index.js +65 -17
  19. package/dist/ios/src/index.js +22 -4
  20. package/dist/ios/templates/CraftApp.swift +473 -60
  21. package/dist/ios/templates/CraftWatchApp.swift.template +8 -0
  22. package/dist/ios/templates/WatchApp.Info.plist.template +2 -0
  23. package/dist/ios/templates/WatchExtension.Info.plist.template +34 -0
  24. package/dist/ios/templates/project.yml.template +10 -4
  25. package/dist/mobile.js +36 -13
  26. package/dist/scaffold-version.d.ts +5 -0
  27. package/package.json +1 -1
  28. package/dist/android/templates/CraftBridgeExtensions.kt.template +0 -383
  29. 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) {
@@ -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();
@@ -1718,25 +1736,149 @@ __export(exports_src2, {
1718
1736
  init: () => init2,
1719
1737
  build: () => build2
1720
1738
  });
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";
1739
+ import { cpSync as cpSync3, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
1740
+ import { dirname as dirname2, extname, isAbsolute, join as join3, relative, resolve as resolve2, sep } from "path";
1741
+ function renderAndroidPromiseRuntime(indent = "") {
1742
+ return ANDROID_PROMISE_RUNTIME.trim().split(`
1743
+ `).map((line) => `${indent}${line}`).join(`
1744
+ `);
1745
+ }
1746
+ function escapeXml(value) {
1747
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
1748
+ }
1749
+ function escapeKotlinString(value) {
1750
+ return value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("$", "\\$").replaceAll("\r", "\\r").replaceAll(`
1751
+ `, "\\n");
1752
+ }
1753
+ function generatedPackageSegment(name) {
1754
+ const normalized = name.toLowerCase().replace(/[^a-z0-9_]/g, "");
1755
+ if (!normalized)
1756
+ return "app";
1757
+ return /^[a-z_]/.test(normalized) ? normalized : `app${normalized}`;
1758
+ }
1759
+ function generatedGradleProjectName(name) {
1760
+ const invalidCharacters = ["/", "\\", ":", "<", ">", '"', "?", "*", "|"];
1761
+ const normalized = invalidCharacters.reduce((value, character) => value.replaceAll(character, "-"), name).trim();
1762
+ return normalized || "craft-app";
1763
+ }
1764
+ function requireRegularFile(path2, label) {
1765
+ if (!existsSync5(path2))
1766
+ throw new Error(`${label} not found: ${path2}`);
1767
+ if (!statSync2(path2).isFile())
1768
+ throw new Error(`${label} must be a file: ${path2}`);
1769
+ }
1770
+ function containsPath(parent, candidate) {
1771
+ const relativePath = relative(parent, candidate);
1772
+ return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
1773
+ }
1774
+ function validateGoogleServicesFile(path2, packageName) {
1775
+ requireRegularFile(path2, "Google services file");
1776
+ let document;
1777
+ try {
1778
+ document = JSON.parse(readFileSync4(path2, "utf8"));
1779
+ } catch (error) {
1780
+ throw new Error(`Google services file must contain valid JSON: ${path2}`, { cause: error });
1781
+ }
1782
+ const matchingClient = document.client?.some((client) => {
1783
+ return client.client_info?.android_client_info?.package_name === packageName;
1784
+ });
1785
+ if (!matchingClient) {
1786
+ throw new Error(`Google services file has no client for Android package ${packageName}: ${path2}`);
1787
+ }
1788
+ }
1789
+ function androidWebUrl(value, field) {
1790
+ let url;
1791
+ try {
1792
+ url = new URL(value);
1793
+ } catch {
1794
+ throw new Error(`Invalid ${field}: ${value}`);
1795
+ }
1796
+ const localDevelopment = url.protocol === "http:" && LOCAL_DEVELOPMENT_HOSTS.has(url.hostname);
1797
+ if (url.protocol !== "https:" && !localDevelopment || url.username || url.password) {
1798
+ throw new Error(`${field} must use HTTPS or local HTTP without credentials: ${value}`);
1799
+ }
1800
+ return url;
1801
+ }
1802
+ function normalizeAndroidNetworkConfig(config) {
1803
+ const schemes = config.urlSchemes?.map((value) => value.trim().toLowerCase()) ?? [];
1804
+ if (schemes.some((value) => !/^[a-z][a-z0-9+.-]*$/.test(value))) {
1805
+ throw new Error("Android deep-link schemes must be valid URI schemes");
1806
+ }
1807
+ config.urlSchemes = [...new Set(schemes)];
1808
+ if (config.enableDeepLinks && config.urlSchemes.length === 0) {
1809
+ throw new Error("Android deep links require at least one URL scheme");
1810
+ }
1811
+ const trustedOrigins = (config.trustedOrigins ?? []).map((value) => {
1812
+ return androidWebUrl(value, "Android trusted origin").origin;
1813
+ });
1814
+ if (config.devServerURL) {
1815
+ const devServer = androidWebUrl(config.devServerURL, "Android dev server URL");
1816
+ config.devServerURL = devServer.toString();
1817
+ trustedOrigins.push(devServer.origin);
1818
+ }
1819
+ config.trustedOrigins = [...new Set(trustedOrigins)];
1820
+ }
1821
+ function validateAndroidConfig(config) {
1822
+ if (!config.appName.trim())
1823
+ throw new Error("Android app name must not be empty");
1824
+ const packageSegments = config.packageName.split(".");
1825
+ if (packageSegments.length < 2 || packageSegments.some((segment) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment) || KOTLIN_KEYWORDS.has(segment))) {
1826
+ throw new Error(`Invalid Android package name: ${config.packageName}`);
1827
+ }
1828
+ if (!/^#(?:[\dA-F]{3,4}|[\dA-F]{6}|[\dA-F]{8})$/i.test(config.backgroundColor ?? "")) {
1829
+ throw new Error(`Invalid Android background color: ${config.backgroundColor}`);
1830
+ }
1831
+ for (const [name, value] of [
1832
+ ["versionCode", config.versionCode],
1833
+ ["minSdk", config.minSdk],
1834
+ ["compileSdk", config.compileSdk],
1835
+ ["targetSdk", config.targetSdk]
1836
+ ]) {
1837
+ if (!Number.isInteger(value) || Number(value) < 1) {
1838
+ throw new Error(`Android ${name} must be a positive integer`);
1839
+ }
1840
+ }
1841
+ if (Number(config.minSdk) > Number(config.targetSdk)) {
1842
+ throw new Error("Android minSdk must not exceed targetSdk");
1843
+ }
1844
+ if (Number(config.targetSdk) > Number(config.compileSdk)) {
1845
+ throw new Error("Android targetSdk must not exceed compileSdk");
1846
+ }
1847
+ }
1848
+ function writeAndroidConfig(output, config) {
1849
+ writeFileSync4(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
1850
+ const runtimeConfig = { ...config };
1851
+ delete runtimeConfig.appIconPath;
1852
+ delete runtimeConfig.googleServicesFile;
1853
+ writeFileSync4(join3(output, "app/src/main/assets/craft.config.json"), JSON.stringify(runtimeConfig, null, 2));
1854
+ }
1723
1855
  function syncAndroidWebAssets(source, output) {
1724
1856
  const sourcePath = resolve2(source);
1725
1857
  if (!existsSync5(sourcePath))
1726
1858
  throw new Error(`Web asset path not found: ${source}`);
1727
- const assetsDir = join3(output, "app/src/main/assets");
1859
+ const sourceStat = statSync2(sourcePath);
1860
+ if (!sourceStat.isDirectory() && !sourceStat.isFile()) {
1861
+ throw new Error(`Web asset path must be a file or directory: ${source}`);
1862
+ }
1863
+ const assetsDir = resolve2(output, "app/src/main/assets");
1864
+ if (containsPath(sourcePath, assetsDir) || containsPath(assetsDir, sourcePath)) {
1865
+ throw new Error(`Web asset source must not overlap generated asset directory: ${source}`);
1866
+ }
1867
+ if (sourceStat.isDirectory()) {
1868
+ requireRegularFile(join3(sourcePath, "index.html"), "Web asset directory entry point");
1869
+ }
1728
1870
  const configPath = join3(assetsDir, "craft.config.json");
1729
- const config = existsSync5(configPath) ? readFileSync3(configPath) : undefined;
1871
+ const config = existsSync5(configPath) ? readFileSync4(configPath) : undefined;
1730
1872
  rmSync3(assetsDir, { recursive: true, force: true });
1731
1873
  mkdirSync3(assetsDir, { recursive: true });
1732
- if (statSync2(sourcePath).isDirectory())
1874
+ if (sourceStat.isDirectory())
1733
1875
  cpSync3(sourcePath, assetsDir, { recursive: true });
1734
1876
  else
1735
1877
  cpSync3(sourcePath, join3(assetsDir, "index.html"));
1736
1878
  if (!existsSync5(join3(assetsDir, "index.html")))
1737
1879
  throw new Error(`Web asset directory must contain index.html: ${source}`);
1738
1880
  if (config)
1739
- writeFileSync3(configPath, config);
1881
+ writeFileSync4(configPath, config);
1740
1882
  }
1741
1883
  function renderAndroidPermissions(config) {
1742
1884
  const permissions = new Set(["android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE"]);
@@ -1792,8 +1934,33 @@ async function init2(options) {
1792
1934
  \u26A1 Initializing Craft Android project: ${name}`);
1793
1935
  console.log(` Output: ${output}
1794
1936
  `);
1795
- const finalPackageName = packageName || `com.craft.${name.toLowerCase().replace(/[^a-z0-9]/g, "")}`;
1937
+ const finalPackageName = packageName || `com.craft.${generatedPackageSegment(name)}`;
1796
1938
  const packagePath = finalPackageName.replace(/\./g, "/");
1939
+ const config = {
1940
+ ...DEFAULT_CONFIG2,
1941
+ ...options.config,
1942
+ appName: name,
1943
+ packageName: finalPackageName
1944
+ };
1945
+ if (config.enableBackgroundLocation)
1946
+ config.enableGeolocation = true;
1947
+ if (config.enableHealthConnect) {
1948
+ config.minSdk = Math.max(config.minSdk ?? 26, 26);
1949
+ config.compileSdk = Math.max(config.compileSdk ?? 36, 36);
1950
+ }
1951
+ normalizeAndroidNetworkConfig(config);
1952
+ validateAndroidConfig(config);
1953
+ if (config.enablePushNotifications && !config.googleServicesFile) {
1954
+ throw new Error("Android push notifications require a googleServicesFile");
1955
+ }
1956
+ if (config.googleServicesFile)
1957
+ validateGoogleServicesFile(config.googleServicesFile, finalPackageName);
1958
+ if (config.appIconPath)
1959
+ requireRegularFile(config.appIconPath, "App icon");
1960
+ const appIconExtension = config.appIconPath ? extname(config.appIconPath).toLowerCase() : undefined;
1961
+ if (config.appIconPath && ![".gif", ".jpg", ".png", ".webp"].includes(appIconExtension ?? "")) {
1962
+ throw new Error(`Unsupported Android app icon format: ${appIconExtension || "(none)"}`);
1963
+ }
1797
1964
  const dirs = [
1798
1965
  output,
1799
1966
  join3(output, "app/src/main/java", packagePath),
@@ -1803,32 +1970,24 @@ async function init2(options) {
1803
1970
  join3(output, "app/src/main/assets"),
1804
1971
  join3(output, "gradle/wrapper")
1805
1972
  ];
1973
+ if (existsSync5(output) && !statSync2(output).isDirectory()) {
1974
+ throw new Error(`Android project output must be a directory: ${output}`);
1975
+ }
1806
1976
  for (const dir of dirs) {
1807
1977
  if (!existsSync5(dir)) {
1808
1978
  mkdirSync3(dir, { recursive: true });
1809
1979
  }
1810
1980
  }
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));
1981
+ writeAndroidConfig(output, config);
1821
1982
  const hasGoogleServices = Boolean(config.googleServicesFile);
1822
1983
  if (config.googleServicesFile) {
1823
- if (!existsSync5(config.googleServicesFile))
1824
- throw new Error(`Google services file not found: ${config.googleServicesFile}`);
1825
1984
  cpSync3(config.googleServicesFile, join3(output, "app/google-services.json"));
1826
1985
  }
1827
- const mainActivityTemplate = readFileSync3(join3(TEMPLATES_DIR2, "MainActivity.kt.template"), "utf-8");
1986
+ const mainActivityTemplate = readFileSync4(join3(TEMPLATES_DIR2, "MainActivity.kt.template"), "utf-8");
1828
1987
  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 {
1988
+ writeFileSync4(join3(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
1989
+ const craftBridgeTemplate = readFileSync4(join3(TEMPLATES_DIR2, "CraftBridge.kt.template"), "utf-8");
1990
+ 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
1991
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
1833
1992
  && ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
1834
1993
  ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 4204)
@@ -1838,27 +1997,26 @@ async function init2(options) {
1838
1997
  val token = if (task.isSuccessful) task.result else null
1839
1998
  val callback = if (token.isNullOrBlank()) "window._craftPushReject" else "window._craftPushResolve"
1840
1999
  val payload = JSONObject.quote(token ?: task.exception?.message ?: "Firebase Cloud Messaging is not configured")
1841
- webView.evaluateJavascript("$callback && $callback($payload)", null)
2000
+ evaluatePromiseJavascript("$callback && $callback($payload)")
1842
2001
  }
1843
2002
  } catch (error: Exception) {
1844
2003
  val payload = JSONObject.quote(error.message ?: "Firebase Cloud Messaging is not configured")
1845
- webView.evaluateJavascript("window._craftPushReject && window._craftPushReject($payload)", null)
2004
+ evaluatePromiseJavascript("window._craftPushReject && window._craftPushReject($payload)")
1846
2005
  }
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>
2006
+ }` : `evaluatePromiseJavascript(
2007
+ "window._craftPushReject && window._craftPushReject('Push notifications are disabled')"
2008
+ )`);
2009
+ writeFileSync4(join3(output, "app/src/main/java", packagePath, "CraftBridge.kt"), craftBridge);
2010
+ const craftNative = readFileSync4(join3(TEMPLATES_DIR2, "CraftNative.kt.template"), "utf-8");
2011
+ const nativeDir = join3(output, "app/src/main/java/com/craft/runtime");
2012
+ mkdirSync3(nativeDir, { recursive: true });
2013
+ writeFileSync4(join3(nativeDir, "CraftNative.kt"), craftNative);
2014
+ const serviceTemplate = readFileSync4(join3(TEMPLATES_DIR2, "LocationRecordingService.kt.template"), "utf-8");
2015
+ writeFileSync4(join3(nativeDir, "LocationRecordingService.kt"), serviceTemplate);
2016
+ const healthTemplate = readFileSync4(join3(TEMPLATES_DIR2, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
2017
+ writeFileSync4(join3(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
2018
+ const manifestTemplate = readFileSync4(join3(TEMPLATES_DIR2, "AndroidManifest.xml.template"), "utf-8");
2019
+ 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
2020
  <package android:name="com.google.android.apps.healthdata" />
1863
2021
  </queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
1864
2022
  <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
@@ -1867,25 +2025,25 @@ async function init2(options) {
1867
2025
  <action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
1868
2026
  <category android:name="android.intent.category.HEALTH_PERMISSIONS" />
1869
2027
  </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")
2028
+ writeFileSync4(join3(output, "app/src/main/AndroidManifest.xml"), manifest);
2029
+ const projectGradleTemplate = readFileSync4(join3(TEMPLATES_DIR2, "build.gradle.kts.project.template"), "utf-8");
2030
+ 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' : ""));
2031
+ const appGradleTemplate = readFileSync4(join3(TEMPLATES_DIR2, "build.gradle.kts.app.template"), "utf-8");
2032
+ 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
2033
  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);
2034
+ writeFileSync4(join3(output, "app/build.gradle.kts"), appGradle);
2035
+ const proguardTemplate = readFileSync4(join3(TEMPLATES_DIR2, "proguard-rules.pro.template"), "utf-8");
2036
+ writeFileSync4(join3(output, "app/proguard-rules.pro"), proguardTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
2037
+ const settingsTemplate = readFileSync4(join3(TEMPLATES_DIR2, "settings.gradle.kts.template"), "utf-8");
2038
+ const settings2 = settingsTemplate.replace(/\{\{APP_NAME\}\}/g, escapeKotlinString(generatedGradleProjectName(name)));
2039
+ writeFileSync4(join3(output, "settings.gradle.kts"), settings2);
1882
2040
  const gradleProperties = `org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
1883
2041
  android.useAndroidX=true
1884
2042
  kotlin.code.style=official
1885
2043
  android.nonTransitiveRClass=true
1886
2044
  `;
1887
- writeFileSync3(join3(output, "gradle.properties"), gradleProperties);
1888
- writeFileSync3(join3(output, "local.properties"), `# SDK location will be set by Android Studio
2045
+ writeFileSync4(join3(output, "gradle.properties"), gradleProperties);
2046
+ writeFileSync4(join3(output, "local.properties"), `# SDK location will be set by Android Studio
1889
2047
  `);
1890
2048
  const gradleWrapperProps = `distributionBase=GRADLE_USER_HOME
1891
2049
  distributionPath=wrapper/dists
@@ -1895,13 +2053,13 @@ validateDistributionUrl=true
1895
2053
  zipStoreBase=GRADLE_USER_HOME
1896
2054
  zipStorePath=wrapper/dists
1897
2055
  `;
1898
- writeFileSync3(join3(output, "gradle/wrapper/gradle-wrapper.properties"), gradleWrapperProps);
2056
+ writeFileSync4(join3(output, "gradle/wrapper/gradle-wrapper.properties"), gradleWrapperProps);
1899
2057
  const stringsXml = `<?xml version="1.0" encoding="utf-8"?>
1900
2058
  <resources>
1901
- <string name="app_name">${name}</string>
2059
+ <string name="app_name">${escapeXml(name)}</string>
1902
2060
  </resources>
1903
2061
  `;
1904
- writeFileSync3(join3(output, "app/src/main/res/values/strings.xml"), stringsXml);
2062
+ writeFileSync4(join3(output, "app/src/main/res/values/strings.xml"), stringsXml);
1905
2063
  const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
1906
2064
  <resources>
1907
2065
  <color name="primary">#1a1a2e</color>
@@ -1910,7 +2068,7 @@ zipStorePath=wrapper/dists
1910
2068
  <color name="background">${config.backgroundColor}</color>
1911
2069
  </resources>
1912
2070
  `;
1913
- writeFileSync3(join3(output, "app/src/main/res/values/colors.xml"), colorsXml);
2071
+ writeFileSync4(join3(output, "app/src/main/res/values/colors.xml"), colorsXml);
1914
2072
  const appIconXml = `<?xml version="1.0" encoding="utf-8"?>
1915
2073
  <vector xmlns:android="http://schemas.android.com/apk/res/android"
1916
2074
  android:width="108dp"
@@ -1922,11 +2080,9 @@ zipStorePath=wrapper/dists
1922
2080
  </vector>
1923
2081
  `;
1924
2082
  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"));
2083
+ cpSync3(config.appIconPath, join3(output, `app/src/main/res/drawable/craft_app_icon${appIconExtension}`));
1928
2084
  } else {
1929
- writeFileSync3(join3(output, "app/src/main/res/drawable/craft_app_icon.xml"), appIconXml);
2085
+ writeFileSync4(join3(output, "app/src/main/res/drawable/craft_app_icon.xml"), appIconXml);
1930
2086
  }
1931
2087
  const themesXml = `<?xml version="1.0" encoding="utf-8"?>
1932
2088
  <resources>
@@ -1937,7 +2093,7 @@ zipStorePath=wrapper/dists
1937
2093
  </style>
1938
2094
  </resources>
1939
2095
  `;
1940
- writeFileSync3(join3(output, "app/src/main/res/values/themes.xml"), themesXml);
2096
+ writeFileSync4(join3(output, "app/src/main/res/values/themes.xml"), themesXml);
1941
2097
  const activityMainXml = `<?xml version="1.0" encoding="utf-8"?>
1942
2098
  <androidx.coordinatorlayout.widget.CoordinatorLayout
1943
2099
  xmlns:android="http://schemas.android.com/apk/res/android"
@@ -1952,13 +2108,13 @@ zipStorePath=wrapper/dists
1952
2108
 
1953
2109
  </androidx.coordinatorlayout.widget.CoordinatorLayout>
1954
2110
  `;
1955
- writeFileSync3(join3(output, "app/src/main/res/layout/activity_main.xml"), activityMainXml);
2111
+ writeFileSync4(join3(output, "app/src/main/res/layout/activity_main.xml"), activityMainXml);
1956
2112
  const placeholderHtml = `<!DOCTYPE html>
1957
2113
  <html>
1958
2114
  <head>
1959
2115
  <meta charset="UTF-8">
1960
2116
  <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>
2117
+ <title>${escapeXml(name)}</title>
1962
2118
  <style>
1963
2119
  * { margin: 0; padding: 0; box-sizing: border-box; }
1964
2120
  body {
@@ -1978,7 +2134,7 @@ zipStorePath=wrapper/dists
1978
2134
  </head>
1979
2135
  <body>
1980
2136
  <div class="container">
1981
- <h1>\u26A1 ${name}</h1>
2137
+ <h1>\u26A1 ${escapeXml(name)}</h1>
1982
2138
  <p>Built with Craft Android</p>
1983
2139
  <p class="ready" id="status">Waiting for Craft bridge...</p>
1984
2140
  </div>
@@ -1990,7 +2146,7 @@ zipStorePath=wrapper/dists
1990
2146
  </script>
1991
2147
  </body>
1992
2148
  </html>`;
1993
- writeFileSync3(join3(output, "app/src/main/assets/index.html"), placeholderHtml);
2149
+ writeFileSync4(join3(output, "app/src/main/assets/index.html"), placeholderHtml);
1994
2150
  console.log("\u2705 Project initialized");
1995
2151
  console.log("");
1996
2152
  console.log("Next steps:");
@@ -2008,19 +2164,21 @@ async function build2(options) {
2008
2164
  if (!existsSync5(configPath)) {
2009
2165
  throw new Error(`No craft.config.json found in ${output}. Run 'craft android init' first.`);
2010
2166
  }
2011
- const config = JSON.parse(readFileSync3(configPath, "utf-8"));
2167
+ const config = JSON.parse(readFileSync4(configPath, "utf-8"));
2012
2168
  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));
2169
+ const url = androidWebUrl(devServer, "Android dev server URL");
2170
+ config.devServerURL = url.toString();
2171
+ config.trustedOrigins = [...new Set([
2172
+ ...(config.trustedOrigins ?? []).map((value) => androidWebUrl(value, "Android trusted origin").origin),
2173
+ url.origin
2174
+ ])];
2175
+ writeAndroidConfig(output, config);
2017
2176
  console.log(` Dev server: ${devServer}`);
2018
2177
  }
2019
2178
  if (htmlPath) {
2020
2179
  syncAndroidWebAssets(htmlPath, output);
2021
2180
  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));
2181
+ writeAndroidConfig(output, config);
2024
2182
  console.log(` Synced: ${htmlPath} \u2192 assets/`);
2025
2183
  }
2026
2184
  if (!compile)
@@ -2075,7 +2233,7 @@ async function run2(options) {
2075
2233
  } else {
2076
2234
  await $2`adb install -r ${apkPath}`;
2077
2235
  }
2078
- const config = JSON.parse(readFileSync3(join3(output, "craft.config.json"), "utf-8"));
2236
+ const config = JSON.parse(readFileSync4(join3(output, "craft.config.json"), "utf-8"));
2079
2237
  const launchCmd = `${config.packageName}/.MainActivity`;
2080
2238
  if (device) {
2081
2239
  await $2`adb -s ${device} shell am start -n ${launchCmd}`;
@@ -2090,10 +2248,106 @@ async function run2(options) {
2090
2248
  throw error;
2091
2249
  }
2092
2250
  }
2093
- var $2, TEMPLATES_DIR2, DEFAULT_CONFIG2;
2251
+ var $2, ANDROID_PROMISE_RUNTIME = `
2252
+ if (window.__craftRejectPendingPromises) {
2253
+ window.__craftRejectPendingPromises('Android bridge reinitialized');
2254
+ }
2255
+ if (window.__craftRejectPermissionRequests) {
2256
+ window.__craftRejectPermissionRequests('Android bridge reinitialized');
2257
+ }
2258
+ window.__craftPromiseRuntimeClosed = false;
2259
+ window.__craftPendingPromises = Object.create(null);
2260
+ window.__craftPromise = function(channel, resolveName, rejectName, invoke, timeoutMs, timeoutError) {
2261
+ if (window.__craftPromiseRuntimeClosed) {
2262
+ return Promise.reject(new Error('Android bridge is closed'));
2263
+ }
2264
+ if (window.__craftPendingPromises[channel]) {
2265
+ return Promise.reject(new Error('A '.concat(channel, ' request is already in progress')));
2266
+ }
2267
+
2268
+ return new Promise(function(resolve, reject) {
2269
+ var entry = {settled: false, timer: null, settle: null};
2270
+ var resolveCallback;
2271
+ var rejectCallback;
2272
+
2273
+ entry.settle = function(succeeded, value) {
2274
+ if (entry.settled || window.__craftPendingPromises[channel] !== entry) return;
2275
+ entry.settled = true;
2276
+ if (entry.timer !== null) clearTimeout(entry.timer);
2277
+ if (window[resolveName] === resolveCallback) window[resolveName] = null;
2278
+ if (window[rejectName] === rejectCallback) window[rejectName] = null;
2279
+ delete window.__craftPendingPromises[channel];
2280
+ if (succeeded) resolve(value);
2281
+ else reject(value);
2282
+ };
2283
+
2284
+ resolveCallback = function(value) {
2285
+ entry.settle(true, value);
2286
+ };
2287
+ rejectCallback = function(error) {
2288
+ entry.settle(false, error);
2289
+ };
2290
+ window[resolveName] = resolveCallback;
2291
+ window[rejectName] = rejectCallback;
2292
+ window.__craftPendingPromises[channel] = entry;
2293
+
2294
+ if (timeoutMs > 0) {
2295
+ entry.timer = setTimeout(function() {
2296
+ entry.settle(false, timeoutError || new Error(channel.concat(' request timed out')));
2297
+ }, timeoutMs);
2298
+ }
2299
+
2300
+ try {
2301
+ invoke();
2302
+ }
2303
+ catch (error) {
2304
+ entry.settle(false, error);
2305
+ }
2306
+ });
2307
+ };
2308
+
2309
+ window.__craftRejectPendingPromises = function(message) {
2310
+ window.__craftPromiseRuntimeClosed = true;
2311
+ Object.keys(window.__craftPendingPromises).forEach(function(channel) {
2312
+ var entry = window.__craftPendingPromises[channel];
2313
+ if (entry) entry.settle(false, new Error(message || 'Android bridge closed'));
2314
+ });
2315
+ };
2316
+ `, TEMPLATES_DIR2, LOCAL_DEVELOPMENT_HOSTS, KOTLIN_KEYWORDS, DEFAULT_CONFIG2;
2094
2317
  var init_src2 = __esm(() => {
2095
2318
  ({ $: $2 } = globalThis.Bun);
2096
2319
  TEMPLATES_DIR2 = join3(dirname2(import.meta.dir), "templates");
2320
+ LOCAL_DEVELOPMENT_HOSTS = new Set(["localhost", "127.0.0.1", "10.0.2.2"]);
2321
+ KOTLIN_KEYWORDS = new Set([
2322
+ "as",
2323
+ "break",
2324
+ "class",
2325
+ "continue",
2326
+ "do",
2327
+ "else",
2328
+ "false",
2329
+ "for",
2330
+ "fun",
2331
+ "if",
2332
+ "in",
2333
+ "interface",
2334
+ "is",
2335
+ "null",
2336
+ "object",
2337
+ "package",
2338
+ "return",
2339
+ "super",
2340
+ "this",
2341
+ "throw",
2342
+ "true",
2343
+ "try",
2344
+ "typealias",
2345
+ "typeof",
2346
+ "val",
2347
+ "var",
2348
+ "when",
2349
+ "while"
2350
+ ]);
2097
2351
  DEFAULT_CONFIG2 = {
2098
2352
  version: "1.0.0",
2099
2353
  versionCode: 1,
@@ -4262,8 +4516,28 @@ function craftBinaryNotFoundMessage(triedPath) {
4262
4516
  ].join(`
4263
4517
  `);
4264
4518
  }
4519
+
4520
+ // src/scaffold-version.ts
4521
+ import { readFileSync, writeFileSync } from "fs";
4522
+ function pinCraftNativeDependency(packagePath, craftVersion) {
4523
+ const manifest = JSON.parse(readFileSync(packagePath, "utf-8"));
4524
+ const sections = [manifest.dependencies, manifest.devDependencies];
4525
+ let found = false;
4526
+ for (const dependencies of sections) {
4527
+ if (dependencies && Object.hasOwn(dependencies, "craft-native")) {
4528
+ dependencies["craft-native"] = `^${craftVersion}`;
4529
+ found = true;
4530
+ }
4531
+ }
4532
+ if (!found) {
4533
+ manifest.dependencies ??= {};
4534
+ manifest.dependencies["craft-native"] = `^${craftVersion}`;
4535
+ }
4536
+ writeFileSync(packagePath, `${JSON.stringify(manifest, null, 2)}
4537
+ `);
4538
+ }
4265
4539
  // package.json
4266
- var version = "0.0.89";
4540
+ var version = "0.0.91";
4267
4541
 
4268
4542
  // bin/cli.ts
4269
4543
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
@@ -4757,7 +5031,7 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
4757
5031
  const template = options?.template || "blank";
4758
5032
  const bundleId = options?.bundleId || `com.example.${name.toLowerCase().replace(/[^a-z0-9]/g, "")}`;
4759
5033
  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");
5034
+ const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync5, existsSync: existsSync7, readdirSync: readdirSync3, readFileSync: readFileSync5, cpSync: cpSync4 } = await import("fs");
4761
5035
  const { join: join4, dirname: dirname3 } = await import("path");
4762
5036
  const replaceVars = (content) => {
4763
5037
  return content.replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{APP_NAME_SLUG\}\}/g, appNameSlug).replace(/\{\{BUNDLE_ID\}\}/g, bundleId).replace(/\{\{AUTHOR\}\}/g, "Developer");
@@ -4779,9 +5053,9 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
4779
5053
  if (entry.isDirectory()) {
4780
5054
  copyRecursive(srcPath, destPath);
4781
5055
  } else {
4782
- const content = readFileSync4(srcPath, "utf-8");
5056
+ const content = readFileSync5(srcPath, "utf-8");
4783
5057
  const processedContent = replaceVars(content);
4784
- writeFileSync4(destPath, processedContent);
5058
+ writeFileSync5(destPath, processedContent);
4785
5059
  }
4786
5060
  }
4787
5061
  };
@@ -4796,7 +5070,7 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
4796
5070
  const copied = await copyTemplate(template, name);
4797
5071
  if (!copied) {
4798
5072
  mkdirSync4(join4(name, "src"), { recursive: true });
4799
- writeFileSync4(join4(name, "index.html"), replaceVars(`<!DOCTYPE html>
5073
+ writeFileSync5(join4(name, "index.html"), replaceVars(`<!DOCTYPE html>
4800
5074
  <html lang="en">
4801
5075
  <head>
4802
5076
  <meta charset="UTF-8">
@@ -4810,7 +5084,7 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
4810
5084
  </div>
4811
5085
  </body>
4812
5086
  </html>`));
4813
- writeFileSync4(join4(name, "package.json"), replaceVars(JSON.stringify({
5087
+ writeFileSync5(join4(name, "package.json"), replaceVars(JSON.stringify({
4814
5088
  name: "{{APP_NAME_SLUG}}",
4815
5089
  version: "1.0.0",
4816
5090
  private: true,
@@ -4820,10 +5094,11 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
4820
5094
  build: "craft build"
4821
5095
  },
4822
5096
  dependencies: {
4823
- "craft-native": "workspace:*"
5097
+ "craft-native": `^${version}`
4824
5098
  }
4825
5099
  }, null, 2)));
4826
5100
  }
5101
+ pinCraftNativeDependency(join4(name, "package.json"), version);
4827
5102
  console.log(`\u2705 ${template} project created`);
4828
5103
  }
4829
5104
  if (template === "desktop" || template === "all") {
@@ -4843,7 +5118,7 @@ export default {
4843
5118
  },
4844
5119
  } satisfies CraftConfig
4845
5120
  `;
4846
- writeFileSync4(`${name}/craft.config.ts`, configContent);
5121
+ writeFileSync5(`${name}/craft.config.ts`, configContent);
4847
5122
  const htmlContent = `<!DOCTYPE html>
4848
5123
  <html lang="en">
4849
5124
  <head>
@@ -4874,7 +5149,7 @@ export default {
4874
5149
  </body>
4875
5150
  </html>
4876
5151
  `;
4877
- writeFileSync4(`${name}/index.html`, htmlContent);
5152
+ writeFileSync5(`${name}/index.html`, htmlContent);
4878
5153
  const packageJson = {
4879
5154
  name: appNameSlug,
4880
5155
  version: "0.1.0",
@@ -4887,10 +5162,11 @@ export default {
4887
5162
  "ios:open": "craft ios open"
4888
5163
  },
4889
5164
  devDependencies: {
4890
- "craft-native": "*"
5165
+ "craft-native": `^${version}`
4891
5166
  }
4892
5167
  };
4893
- writeFileSync4(`${name}/package.json`, JSON.stringify(packageJson, null, 2));
5168
+ writeFileSync5(`${name}/package.json`, JSON.stringify(packageJson, null, 2));
5169
+ pinCraftNativeDependency(`${name}/package.json`, version);
4894
5170
  console.log("\u2705 Desktop project created");
4895
5171
  }
4896
5172
  if (template === "ios" || template === "all") {