@craft-native/ios 0.0.153 → 0.0.155

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -602,8 +602,12 @@ await fetch('https://example.com/upload', { method: 'POST', body: form });
602
602
  // A file for the share sheet (Save to Files, AirDrop, another app), fetched
603
603
  // straight to disk with your headers. Not 2xx: { ok: false, status, body }.
604
604
  const gpx = await craft.files.download('https://example.com/route.gpx', { headers: { Authorization: 'Bearer …' } });
605
- if (gpx.ok) await craft.share({ files: [gpx.path] });
605
+ if (gpx.ok) await craft.share({ files: [gpx] }); // a file, or its path
606
606
  await craft.files.remove(gpx.path); // or leave it: tmp, swept after a day
607
+ // Something the screen made itself (a CSV export): text crosses the bridge
608
+ // once, as it is; bytes go as base64 ({ encoding: 'base64' }).
609
+ const csv = await craft.files.write('health-data.csv', 'date,steps\n…', { type: 'text/csv' });
610
+ await craft.share({ files: [csv] });
607
611
 
608
612
  // Speech: short spoken cues that duck the person's music instead of stopping
609
613
  // it, and play with the silent switch on. Always available (no config flag).
package/dist/cli.js CHANGED
@@ -6,12 +6,18 @@ import { parseArgs } from "util";
6
6
 
7
7
  // src/index.ts
8
8
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
9
- import { dirname, join, resolve } from "path";
9
+ import { dirname as dirname2, join as join2, resolve } from "path";
10
10
  var {$ } = globalThis.Bun;
11
- var TEMPLATES_DIR = join(dirname(import.meta.dir), "templates");
11
+
12
+ // src/testing.ts
13
+ import { dirname, join } from "path";
14
+ var templates = join(dirname(import.meta.dir), "templates/testing");
15
+
16
+ // src/index.ts
17
+ var TEMPLATES_DIR = join2(dirname2(import.meta.dir), "templates");
12
18
  var CRAFT_VERSION = (() => {
13
19
  try {
14
- return JSON.parse(readFileSync(join(dirname(import.meta.dir), "package.json"), "utf8")).version ?? "0.0.0";
20
+ return JSON.parse(readFileSync(join2(dirname2(import.meta.dir), "package.json"), "utf8")).version ?? "0.0.0";
15
21
  } catch {
16
22
  return "0.0.0";
17
23
  }
@@ -67,8 +73,8 @@ function hybridConfigProblems(config) {
67
73
  function installNativeBundle(source, output) {
68
74
  if (!existsSync(source))
69
75
  throw new Error(`Native bundle not found: ${source}`);
70
- const destination = join(output, "dist", "native-screen.js");
71
- mkdirSync(dirname(destination), { recursive: true });
76
+ const destination = join2(output, "dist", "native-screen.js");
77
+ mkdirSync(dirname2(destination), { recursive: true });
72
78
  if (resolve(source) !== resolve(destination))
73
79
  cpSync(source, destination);
74
80
  }
@@ -432,20 +438,20 @@ function writeOpaqueIcon(source, target) {
432
438
  }
433
439
  }
434
440
  function renderAssetCatalog(output, config) {
435
- const catalog = join(output, "Assets.xcassets");
436
- const appIcon = join(catalog, "AppIcon.appiconset");
437
- const launchBackground = join(catalog, "LaunchBackground.colorset");
441
+ const catalog = join2(output, "Assets.xcassets");
442
+ const appIcon = join2(catalog, "AppIcon.appiconset");
443
+ const launchBackground = join2(catalog, "LaunchBackground.colorset");
438
444
  mkdirSync(appIcon, { recursive: true });
439
445
  mkdirSync(launchBackground, { recursive: true });
440
- writeFileSync(join(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
446
+ writeFileSync(join2(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
441
447
  `);
442
448
  const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
443
449
  if (config.appIconPath) {
444
450
  if (!existsSync(config.appIconPath))
445
451
  throw new Error(`App icon not found: ${config.appIconPath}`);
446
- writeOpaqueIcon(config.appIconPath, join(appIcon, iconFilename));
452
+ writeOpaqueIcon(config.appIconPath, join2(appIcon, iconFilename));
447
453
  }
448
- writeFileSync(join(appIcon, "Contents.json"), `${JSON.stringify({
454
+ writeFileSync(join2(appIcon, "Contents.json"), `${JSON.stringify({
449
455
  images: iconFilename ? [{
450
456
  filename: iconFilename,
451
457
  idiom: "universal",
@@ -472,7 +478,7 @@ function renderAssetCatalog(output, config) {
472
478
  idiom: "universal"
473
479
  });
474
480
  }
475
- writeFileSync(join(launchBackground, "Contents.json"), `${JSON.stringify({
481
+ writeFileSync(join2(launchBackground, "Contents.json"), `${JSON.stringify({
476
482
  colors,
477
483
  info: { author: "xcode", version: 1 }
478
484
  }, null, 2)}
@@ -481,7 +487,7 @@ function renderAssetCatalog(output, config) {
481
487
  }
482
488
  var LAUNCH_LOGO = "LaunchLogo";
483
489
  function renderLaunchLogo(catalog, config) {
484
- const imageset = join(catalog, `${LAUNCH_LOGO}.imageset`);
490
+ const imageset = join2(catalog, `${LAUNCH_LOGO}.imageset`);
485
491
  rmSync(imageset, { recursive: true, force: true });
486
492
  if (!config.splashImagePath)
487
493
  return;
@@ -497,14 +503,14 @@ function renderLaunchLogo(catalog, config) {
497
503
  if (extension === "svg" || extension === "pdf")
498
504
  vector = true;
499
505
  const filename = `${LAUNCH_LOGO}${dark ? "-dark" : ""}.${extension}`;
500
- cpSync(source, join(imageset, filename));
506
+ cpSync(source, join2(imageset, filename));
501
507
  images.push({
502
508
  ...dark ? { appearances: [{ appearance: "luminosity", value: "dark" }] } : {},
503
509
  filename,
504
510
  idiom: "universal"
505
511
  });
506
512
  }
507
- writeFileSync(join(imageset, "Contents.json"), `${JSON.stringify({
513
+ writeFileSync(join2(imageset, "Contents.json"), `${JSON.stringify({
508
514
  images,
509
515
  info: { author: "xcode", version: 1 },
510
516
  ...vector ? { properties: { "preserves-vector-representation": true } } : {}
@@ -618,15 +624,15 @@ function syncWebAssets(source, output) {
618
624
  const sourcePath = resolve(source);
619
625
  if (!existsSync(sourcePath))
620
626
  throw new Error(`Web asset path not found: ${source}`);
621
- const distDir = join(output, "dist");
627
+ const distDir = join2(output, "dist");
622
628
  rmSync(distDir, { recursive: true, force: true });
623
629
  mkdirSync(distDir, { recursive: true });
624
630
  if (statSync(sourcePath).isDirectory()) {
625
631
  cpSync(sourcePath, distDir, { recursive: true });
626
632
  } else {
627
- cpSync(sourcePath, join(distDir, "index.html"));
633
+ cpSync(sourcePath, join2(distDir, "index.html"));
628
634
  }
629
- if (!existsSync(join(distDir, "index.html"))) {
635
+ if (!existsSync(join2(distDir, "index.html"))) {
630
636
  throw new Error(`Web asset directory must contain index.html: ${source}`);
631
637
  }
632
638
  }
@@ -648,32 +654,32 @@ var RUNTIME_ARCHIVES = {
648
654
  };
649
655
  async function installRuntime(output, runtimeDir) {
650
656
  const resolved = Object.entries(RUNTIME_ARCHIVES).map(([sdk, archives]) => {
651
- const present = archives.filter((a) => existsSync(join(runtimeDir, a)));
657
+ const present = archives.filter((a) => existsSync(join2(runtimeDir, a)));
652
658
  if (present.length === 0) {
653
659
  throw new Error(`${runtimeDir} has none of ${archives.join(", ")}. ` + `Run \`zig build build-ios-all\` in packages/zig and point at its zig-out/lib.`);
654
660
  }
655
661
  return { sdk, archives, present };
656
662
  });
657
- const dest = join(output, "Runtime");
663
+ const dest = join2(output, "Runtime");
658
664
  rmSync(dest, { recursive: true, force: true });
659
665
  for (const { sdk, archives, present } of resolved) {
660
- const sdkDir = join(dest, sdk);
666
+ const sdkDir = join2(dest, sdk);
661
667
  mkdirSync(sdkDir, { recursive: true });
662
- const target = join(sdkDir, "libcraft-ios.a");
668
+ const target = join2(sdkDir, "libcraft-ios.a");
663
669
  if (present.length === 1) {
664
670
  const missing = archives.filter((a) => !present.includes(a));
665
671
  if (missing.length > 0) {
666
672
  console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
667
673
  }
668
- cpSync(join(runtimeDir, present[0]), target);
674
+ cpSync(join2(runtimeDir, present[0]), target);
669
675
  } else {
670
- await $`lipo -create ${present.map((a) => join(runtimeDir, a))} -output ${target}`.quiet();
676
+ await $`lipo -create ${present.map((a) => join2(runtimeDir, a))} -output ${target}`.quiet();
671
677
  }
672
678
  }
673
679
  return true;
674
680
  }
675
681
  async function refreshRuntime(output, override) {
676
- if (!existsSync(join(output, "Runtime")))
682
+ if (!existsSync(join2(output, "Runtime")))
677
683
  return;
678
684
  const dir = resolveRuntimeDir(override);
679
685
  if (!dir) {
@@ -690,6 +696,39 @@ function productName(name) {
690
696
  return "Craft";
691
697
  return /^\d/.test(joined) ? `App${joined}` : joined;
692
698
  }
699
+ function writeShell(output, config) {
700
+ if (!config.appName || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(config.bundleId))
701
+ throw new Error("The iOS shell needs an appName and a valid bundleId in craft.config.json.");
702
+ const name = productName(config.appName);
703
+ mkdirSync(join2(output, "Sources"), { recursive: true });
704
+ mkdirSync(join2(output, "Shared"), { recursive: true });
705
+ const swiftTemplate = readFileSync(join2(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
706
+ const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, config.bundleId).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION);
707
+ writeFileSync(join2(output, "Sources", `${name}App.swift`), swiftSource);
708
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeScreen.swift"), join2(output, "Sources", "CraftNativeScreen.swift"));
709
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join2(output, "Sources", "CraftNativeFlatList.swift"));
710
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeActions.swift"), join2(output, "Sources", "CraftNativeActions.swift"));
711
+ cpSync(join2(TEMPLATES_DIR, "CraftHealth.swift"), join2(output, "Sources", "CraftHealth.swift"));
712
+ cpSync(join2(TEMPLATES_DIR, "CraftMusic.swift"), join2(output, "Sources", "CraftMusic.swift"));
713
+ cpSync(join2(TEMPLATES_DIR, "CraftHeartRate.swift"), join2(output, "Sources", "CraftHeartRate.swift"));
714
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeMutation.swift"), join2(output, "Sources", "CraftNativeMutation.swift"));
715
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeUI.swift"), join2(output, "Sources", "CraftNativeUI.swift"));
716
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeServices.swift"), join2(output, "Sources", "CraftNativeServices.swift"));
717
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeFiles.swift"), join2(output, "Sources", "CraftNativeFiles.swift"));
718
+ cpSync(join2(TEMPLATES_DIR, "CraftWebAudio.swift"), join2(output, "Sources", "CraftWebAudio.swift"));
719
+ cpSync(join2(TEMPLATES_DIR, "CraftHybrid.swift"), join2(output, "Sources", "CraftHybrid.swift"));
720
+ cpSync(join2(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join2(output, "Shared", "CraftActivityAttributes.swift"));
721
+ for (const [enabled, folder, template, filename] of [
722
+ [config.enableLiveActivities, "WidgetExtension", "CraftLiveActivityWidget.swift.template", `${name}LiveActivity.swift`],
723
+ [config.enableWatchApp, "WatchExtension", "CraftWatchApp.swift.template", `${name}WatchApp.swift`]
724
+ ]) {
725
+ if (!enabled)
726
+ continue;
727
+ mkdirSync(join2(output, folder), { recursive: true });
728
+ const source = readFileSync(join2(TEMPLATES_DIR, template), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
729
+ writeFileSync(join2(output, folder, filename), source);
730
+ }
731
+ }
693
732
  async function init(options) {
694
733
  const { name: displayName, bundleId, teamId, output } = options;
695
734
  const name = productName(displayName);
@@ -697,9 +736,9 @@ async function init(options) {
697
736
  \u26A1 Initializing Craft iOS project: ${displayName}`);
698
737
  console.log(` Output: ${output}
699
738
  `);
700
- const dirs = [output, join(output, "Sources"), join(output, "Shared"), join(output, "dist")];
739
+ const dirs = [output, join2(output, "Sources"), join2(output, "Shared"), join2(output, "dist")];
701
740
  if (options.config?.enableWatchApp)
702
- dirs.push(join(output, "WatchApp"), join(output, "WatchExtension"));
741
+ dirs.push(join2(output, "WatchApp"), join2(output, "WatchExtension"));
703
742
  for (const dir of dirs) {
704
743
  if (!existsSync(dir)) {
705
744
  mkdirSync(dir, { recursive: true });
@@ -726,29 +765,15 @@ async function init(options) {
726
765
  `));
727
766
  if (Object.keys(config.shareStorage ?? {}).length)
728
767
  config.enableSecureStorage = true;
729
- writeFileSync(join(output, "craft.config.json"), JSON.stringify(config, null, 2));
730
- const swiftTemplate = readFileSync(join(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
731
- const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION);
732
- writeFileSync(join(output, "Sources", `${name}App.swift`), swiftSource);
733
- cpSync(join(TEMPLATES_DIR, "CraftNativeScreen.swift"), join(output, "Sources", "CraftNativeScreen.swift"));
734
- cpSync(join(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join(output, "Sources", "CraftNativeFlatList.swift"));
735
- cpSync(join(TEMPLATES_DIR, "CraftNativeActions.swift"), join(output, "Sources", "CraftNativeActions.swift"));
736
- cpSync(join(TEMPLATES_DIR, "CraftHealth.swift"), join(output, "Sources", "CraftHealth.swift"));
737
- cpSync(join(TEMPLATES_DIR, "CraftMusic.swift"), join(output, "Sources", "CraftMusic.swift"));
738
- cpSync(join(TEMPLATES_DIR, "CraftHeartRate.swift"), join(output, "Sources", "CraftHeartRate.swift"));
739
- cpSync(join(TEMPLATES_DIR, "CraftNativeMutation.swift"), join(output, "Sources", "CraftNativeMutation.swift"));
740
- cpSync(join(TEMPLATES_DIR, "CraftNativeUI.swift"), join(output, "Sources", "CraftNativeUI.swift"));
741
- cpSync(join(TEMPLATES_DIR, "CraftNativeServices.swift"), join(output, "Sources", "CraftNativeServices.swift"));
742
- cpSync(join(TEMPLATES_DIR, "CraftNativeFiles.swift"), join(output, "Sources", "CraftNativeFiles.swift"));
743
- cpSync(join(TEMPLATES_DIR, "CraftWebAudio.swift"), join(output, "Sources", "CraftWebAudio.swift"));
744
- cpSync(join(TEMPLATES_DIR, "CraftHybrid.swift"), join(output, "Sources", "CraftHybrid.swift"));
745
- const infoPlistTemplate = readFileSync(join(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
768
+ writeFileSync(join2(output, "craft.config.json"), JSON.stringify(config, null, 2));
769
+ writeShell(output, config);
770
+ const infoPlistTemplate = readFileSync(join2(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
746
771
  const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, renderAppearance(config).interfaceStyle).replace(/\{\{STATUS_BAR_STYLE\}\}/g, renderAppearance(config).statusBarStyle).replace(/\{\{ORIENTATIONS\}\}/g, renderOrientations(config)).replace(/\{\{USAGE_DESCRIPTIONS\}\}/g, renderUsageDescriptions(config)).replace(/\{\{URL_TYPES\}\}/g, renderUrlTypes(config)).replace(/\{\{APP_BOUND_DOMAINS\}\}/g, renderAppBoundDomains(config)).replace(/\{\{BACKGROUND_MODES\}\}/g, renderBackgroundModes(config)).replace(/\{\{BACKGROUND_TASK_IDENTIFIERS\}\}/g, renderBackgroundTaskIdentifiers(config)).replace(/\{\{LAUNCH_SCREEN\}\}/g, renderLaunchScreen(config)).replace(/\{\{NON_EXEMPT_ENCRYPTION\}\}/g, config.usesNonExemptEncryption ? "true" : "false").replace(/\{\{LIVE_ACTIVITY_SUPPORT\}\}/g, config.enableLiveActivities ? ` <key>NSSupportsLiveActivities</key>
747
772
  <true/>
748
773
  <key>NSSupportsLiveActivitiesFrequentUpdates</key>
749
774
  <true/>` : "");
750
- writeFileSync(join(output, "Info.plist"), infoPlist);
751
- const projectYmlTemplate = readFileSync(join(TEMPLATES_DIR, "project.yml.template"), "utf-8");
775
+ writeFileSync(join2(output, "Info.plist"), infoPlist);
776
+ const projectYmlTemplate = readFileSync(join2(TEMPLATES_DIR, "project.yml.template"), "utf-8");
752
777
  const nativeDependencies = [];
753
778
  if (config.enableLiveActivities)
754
779
  nativeDependencies.push(` - target: ${name}LiveActivity`);
@@ -805,35 +830,30 @@ async function init(options) {
805
830
  if (hasRuntime) {
806
831
  console.log(" Linked the Zig runtime from", runtimeDir);
807
832
  } else {
808
- rmSync(join(output, "Runtime"), { recursive: true, force: true });
833
+ rmSync(join2(output, "Runtime"), { recursive: true, force: true });
809
834
  }
810
835
  const projectYml = projectYmlTemplate.replace(/\{\{CRAFT_RUNTIME_SETTINGS\}\}/g, hasRuntime ? renderRuntimeSettings() : "").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{BUNDLE_ID_PREFIX\}\}/g, bundleIdPrefix).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{IOS_VERSION\}\}/g, config.iosVersion || "15.0").replace(/\{\{DEVICE_FAMILIES\}\}/g, renderDeviceFamilies(config)).replace(/\{\{TEAM_ID\}\}/g, teamId || "").replace(/\{\{NATIVE_DEPENDENCIES\}\}/g, nativeDependencies.length ? ` dependencies:
811
836
  ${nativeDependencies.join(`
812
837
  `)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
813
838
  `));
814
- writeFileSync(join(output, "project.yml"), projectYml);
815
- writeFileSync(join(output, "Craft.entitlements"), renderEntitlements(config));
816
- writeFileSync(join(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
839
+ writeFileSync(join2(output, "project.yml"), projectYml);
840
+ writeFileSync(join2(output, "Craft.entitlements"), renderEntitlements(config));
841
+ writeFileSync(join2(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
817
842
  renderAssetCatalog(output, config);
818
- cpSync(join(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join(output, "Shared", "CraftActivityAttributes.swift"));
819
843
  if (config.enableLiveActivities) {
820
- mkdirSync(join(output, "WidgetExtension"), { recursive: true });
821
- const widgetSource = readFileSync(join(TEMPLATES_DIR, "CraftLiveActivityWidget.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
822
- writeFileSync(join(output, "WidgetExtension", `${name}LiveActivity.swift`), widgetSource);
823
- const widgetInfo = readFileSync(join(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
824
- writeFileSync(join(output, "WidgetExtension", "Info.plist"), widgetInfo);
844
+ mkdirSync(join2(output, "WidgetExtension"), { recursive: true });
845
+ const widgetInfo = readFileSync(join2(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
846
+ writeFileSync(join2(output, "WidgetExtension", "Info.plist"), widgetInfo);
825
847
  }
826
848
  if (config.enableWatchApp) {
827
- const watchSource = readFileSync(join(TEMPLATES_DIR, "CraftWatchApp.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
828
- writeFileSync(join(output, "WatchExtension", `${name}WatchApp.swift`), watchSource);
829
- const watchInfo = readFileSync(join(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
830
- writeFileSync(join(output, "WatchApp", "Info.plist"), watchInfo);
831
- writeFileSync(join(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
832
- const watchExtensionInfo = readFileSync(join(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
833
- writeFileSync(join(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
834
- writeFileSync(join(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
849
+ const watchInfo = readFileSync(join2(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
850
+ writeFileSync(join2(output, "WatchApp", "Info.plist"), watchInfo);
851
+ writeFileSync(join2(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
852
+ const watchExtensionInfo = readFileSync(join2(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
853
+ writeFileSync(join2(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
854
+ writeFileSync(join2(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
835
855
  }
836
- writeFileSync(join(output, "dist", "index.html"), renderOfflinePage(config));
856
+ writeFileSync(join2(output, "dist", "index.html"), renderOfflinePage(config));
837
857
  if (isHybrid(config)) {
838
858
  if (config.nativeBundle)
839
859
  installNativeBundle(config.nativeBundle, output);
@@ -858,7 +878,7 @@ async function build(options) {
858
878
  const { htmlPath, nativeBundlePath, devServer, output, generateProject = true } = options;
859
879
  console.log(`
860
880
  \uD83D\uDCE6 Building Craft iOS project...`);
861
- const configPath = join(output, "craft.config.json");
881
+ const configPath = join2(output, "craft.config.json");
862
882
  if (!existsSync(configPath)) {
863
883
  throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
864
884
  }
@@ -871,11 +891,11 @@ async function build(options) {
871
891
  throw new Error("A native iOS screen cannot use --html-path or --dev-server.");
872
892
  }
873
893
  if (nativeBundlePath) {
874
- const destination = join(output, "dist", "native-screen.js");
894
+ const destination = join2(output, "dist", "native-screen.js");
875
895
  if (resolve(nativeBundlePath) !== resolve(destination))
876
896
  cpSync(nativeBundlePath, destination);
877
897
  }
878
- if (!existsSync(join(output, "dist", "native-screen.js"))) {
898
+ if (!existsSync(join2(output, "dist", "native-screen.js"))) {
879
899
  throw new Error("Native iOS mode needs dist/native-screen.js. Compile a .stx screen with `stx native compile` first.");
880
900
  }
881
901
  } else if (nativeBundlePath && !isHybrid(config)) {
@@ -901,7 +921,7 @@ async function build(options) {
901
921
  if (bundle) {
902
922
  installNativeBundle(bundle, output);
903
923
  console.log(` Native screens: ${bundle} \u2192 dist/native-screen.js`);
904
- } else if (!existsSync(join(output, "dist", "native-screen.js"))) {
924
+ } else if (!existsSync(join2(output, "dist", "native-screen.js"))) {
905
925
  console.warn(" \u26A0 nativeScreens is set but there is no native bundle: every path opens in the web view.");
906
926
  }
907
927
  }
@@ -931,7 +951,7 @@ async function open(options) {
931
951
  if (!xcodeproj) {
932
952
  throw new Error(`No Xcode project found in ${output}. Run 'craft ios build' first.`);
933
953
  }
934
- const projectPath = join(output, xcodeproj);
954
+ const projectPath = join2(output, xcodeproj);
935
955
  console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
936
956
  await $`open ${projectPath}`;
937
957
  }
@@ -972,7 +992,7 @@ async function showSimulator() {
972
992
  const selected = await $`xcode-select -p`.quiet().nothrow();
973
993
  const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
974
994
  if (developer) {
975
- const app = join(developer, "Applications", "Simulator.app");
995
+ const app = join2(developer, "Applications", "Simulator.app");
976
996
  if (existsSync(app)) {
977
997
  await $`open ${app}`.quiet().nothrow();
978
998
  return;
@@ -988,9 +1008,9 @@ async function run(options) {
988
1008
  if (!xcodeproj) {
989
1009
  throw new Error(`No Xcode project found in ${output}`);
990
1010
  }
991
- const projectPath = join(output, xcodeproj);
1011
+ const projectPath = join2(output, xcodeproj);
992
1012
  const appName = xcodeproj.replace(".xcodeproj", "");
993
- const configPath = join(output, "craft.config.json");
1013
+ const configPath = join2(output, "craft.config.json");
994
1014
  if (!existsSync(configPath))
995
1015
  throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
996
1016
  const config = JSON.parse(readFileSync(configPath, "utf-8"));
@@ -1003,8 +1023,8 @@ async function run(options) {
1003
1023
  console.log(` Device: ${device.name} (${device.runtime})`);
1004
1024
  try {
1005
1025
  await bootSimulator(device);
1006
- const derivedData = join(output, "build");
1007
- const product = join(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
1026
+ const derivedData = join2(output, "build");
1027
+ const product = join2(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
1008
1028
  await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
1009
1029
  if (!existsSync(product)) {
1010
1030
  throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
package/dist/index.d.ts CHANGED
@@ -412,6 +412,8 @@ export declare function installRuntime(output: string, runtimeDir: string): Prom
412
412
  * valid Swift type.
413
413
  */
414
414
  export declare function productName(name: string): string;
415
+ /** Restore package-owned Swift into an existing project, without changing its Xcode metadata or bundled screens. */
416
+ export declare function restoreShell(output: string): void;
415
417
  export declare function init(options: InitOptions): Promise<void>;
416
418
  /**
417
419
  * Build web assets and generate Xcode project
@@ -475,3 +477,5 @@ export declare function bootSimulator(device: SimulatorDevice): Promise<void>;
475
477
  */
476
478
  export declare function showSimulator(): Promise<void>;
477
479
  export declare function run(options: RunOptions): Promise<void>;
480
+ export { createNativeMutationValidator, initSimulatorDriver, serializeSimulatorSteps } from './testing.js';
481
+ export type { NativeMutationValidator, NativeValidationResult, SimulatorDriverOptions, SimulatorStep } from './testing.js';