@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/dist/index.js CHANGED
@@ -1,12 +1,149 @@
1
1
  // @bun
2
2
  // src/index.ts
3
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
4
- import { dirname, join, resolve } from "path";
3
+ import { cpSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
4
+ import { dirname as dirname2, join as join2, resolve } from "path";
5
5
  var {$ } = globalThis.Bun;
6
- var TEMPLATES_DIR = join(dirname(import.meta.dir), "templates");
6
+
7
+ // src/testing.ts
8
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
9
+ import { tmpdir } from "os";
10
+ import { dirname, join } from "path";
11
+ import { spawnSync } from "child_process";
12
+ var templates = join(dirname(import.meta.dir), "templates/testing");
13
+ function bundleIdentifier(value) {
14
+ if (!/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(value))
15
+ throw new Error(`Invalid bundle identifier: ${value}`);
16
+ }
17
+ function swiftString(value) {
18
+ return JSON.stringify(value).replace(/\\u([a-f\d]{4})/gi, "\\u{$1}");
19
+ }
20
+ function initSimulatorDriver(options) {
21
+ const { output, bundleId } = options;
22
+ const name = options.projectName ?? "CraftDriver";
23
+ const prefix = options.bundleIdPrefix ?? `${bundleId}.driver`;
24
+ const version = options.deploymentTarget ?? "17.0";
25
+ const control = options.controlUrl ?? "http://127.0.0.1:3401";
26
+ bundleIdentifier(bundleId);
27
+ bundleIdentifier(prefix);
28
+ if (!/^[A-Za-z][A-Za-z0-9]*$/.test(name))
29
+ throw new Error("projectName must be an alphanumeric Xcode name starting with a letter");
30
+ if (!/^\d+\.\d+$/.test(version) || Number(version) < 17)
31
+ throw new Error("The simulator driver requires iOS 17.0 or newer");
32
+ if (!["http:", "https:"].includes(new URL(control).protocol))
33
+ throw new Error("controlUrl must use HTTP or HTTPS");
34
+ const replacements = {
35
+ PROJECT_NAME: name,
36
+ BUNDLE_PREFIX: prefix,
37
+ IOS_VERSION: version,
38
+ BUNDLE_ID: swiftString(bundleId),
39
+ CONTROL_URL: swiftString(control),
40
+ TABS: `[${(options.tabs ?? []).map(swiftString).join(", ")}]`
41
+ };
42
+ for (const [source, destination] of [
43
+ ["HostApp.swift", "Host/HostApp.swift"],
44
+ ["DriverTests.swift.template", "UITests/DriverTests.swift"],
45
+ ["project.yml.template", "project.yml"]
46
+ ]) {
47
+ const path = join(output, destination);
48
+ mkdirSync(dirname(path), { recursive: true });
49
+ const content = readFileSync(join(templates, source), "utf8").replace(/\{\{([A-Z_]+)\}\}/g, (_, key) => replacements[key]);
50
+ let previous;
51
+ try {
52
+ previous = readFileSync(path, "utf8");
53
+ } catch {}
54
+ if (previous !== content)
55
+ writeFileSync(path, content);
56
+ }
57
+ }
58
+ function serializeSimulatorSteps(steps) {
59
+ const number = (value) => {
60
+ if (!Number.isFinite(value) || value < 0)
61
+ throw new Error("Simulator values must be finite and nonnegative");
62
+ return value;
63
+ };
64
+ const token = (value) => {
65
+ if (!value || /\s/.test(value))
66
+ throw new Error("Simulator identifiers must be nonempty single tokens");
67
+ return value;
68
+ };
69
+ return steps.map((step) => {
70
+ let arg = "";
71
+ if ("seconds" in step && !("text" in step))
72
+ arg = String(number(step.seconds));
73
+ else if ("milliseconds" in step)
74
+ arg = String(number(step.milliseconds));
75
+ else if ("state" in step)
76
+ arg = step.state;
77
+ else if ("direction" in step)
78
+ arg = step.direction;
79
+ else if ("x" in step) {
80
+ if (number(step.x) > 1 || number(step.y) > 1)
81
+ throw new Error("tapxy coordinates must be between 0 and 1");
82
+ arg = `${step.x} ${step.y}`;
83
+ } else if ("index" in step) {
84
+ if (!Number.isInteger(number(step.index)))
85
+ throw new Error("field index must be an integer");
86
+ arg = `${step.index} ${step.text}`;
87
+ } else if ("id" in step)
88
+ arg = `${token(step.id)} ${step.text}`;
89
+ else if ("text" in step) {
90
+ arg = step.text;
91
+ if ("seconds" in step && step.seconds !== undefined) {
92
+ if (arg.includes("|"))
93
+ throw new Error("A timed expectation cannot contain |");
94
+ arg += ` | ${number(step.seconds)}`;
95
+ }
96
+ } else if ("name" in step)
97
+ arg = token(step.name);
98
+ else if ("url" in step)
99
+ arg = step.url;
100
+ if (/[\r\n]/.test(arg))
101
+ throw new Error("Simulator arguments cannot contain newlines");
102
+ return `${step.command}${arg ? ` ${arg}` : ""}`;
103
+ }).join(`
104
+ `) + `
105
+ `;
106
+ }
107
+ function createNativeMutationValidator() {
108
+ const output = mkdtempSync(join(tmpdir(), "craft-native-mutation-"));
109
+ const binary = join(output, "validate");
110
+ try {
111
+ writeFileSync(join(output, "main.swift"), readFileSync(join(templates, "MutationValidator.swift")));
112
+ const build = Bun.spawnSync(["swiftc", join(templates, "../CraftNativeMutation.swift"), join(output, "main.swift"), "-o", binary]);
113
+ if (build.exitCode !== 0)
114
+ throw new Error(build.stderr.toString());
115
+ } catch (error) {
116
+ rmSync(output, { recursive: true, force: true });
117
+ throw error;
118
+ }
119
+ let disposed = false;
120
+ return {
121
+ validate(messages) {
122
+ if (disposed)
123
+ throw new Error("The native mutation validator has been disposed");
124
+ const input = join(output, "messages.json");
125
+ writeFileSync(input, JSON.stringify(messages));
126
+ const report = join(output, "result.txt");
127
+ rmSync(report, { force: true });
128
+ const result = spawnSync(binary, [input, report], { encoding: "utf8" });
129
+ let stdout = result.stdout ?? "";
130
+ try {
131
+ stdout = readFileSync(report, "utf8");
132
+ } catch {}
133
+ return { exitCode: result.status ?? 1, stdout, stderr: result.stderr || result.error?.message || (result.signal ? `Validator terminated by ${result.signal}` : "") };
134
+ },
135
+ dispose() {
136
+ disposed = true;
137
+ rmSync(output, { recursive: true, force: true });
138
+ }
139
+ };
140
+ }
141
+
142
+ // src/index.ts
143
+ var TEMPLATES_DIR = join2(dirname2(import.meta.dir), "templates");
7
144
  var CRAFT_VERSION = (() => {
8
145
  try {
9
- return JSON.parse(readFileSync(join(dirname(import.meta.dir), "package.json"), "utf8")).version ?? "0.0.0";
146
+ return JSON.parse(readFileSync2(join2(dirname2(import.meta.dir), "package.json"), "utf8")).version ?? "0.0.0";
10
147
  } catch {
11
148
  return "0.0.0";
12
149
  }
@@ -122,8 +259,8 @@ function hybridConfigProblems(config) {
122
259
  function installNativeBundle(source, output) {
123
260
  if (!existsSync(source))
124
261
  throw new Error(`Native bundle not found: ${source}`);
125
- const destination = join(output, "dist", "native-screen.js");
126
- mkdirSync(dirname(destination), { recursive: true });
262
+ const destination = join2(output, "dist", "native-screen.js");
263
+ mkdirSync2(dirname2(destination), { recursive: true });
127
264
  if (resolve(source) !== resolve(destination))
128
265
  cpSync(source, destination);
129
266
  }
@@ -471,7 +608,7 @@ function pngHasAlpha(bytes) {
471
608
  return false;
472
609
  }
473
610
  function writeOpaqueIcon(source, target) {
474
- const bytes = new Uint8Array(readFileSync(source));
611
+ const bytes = new Uint8Array(readFileSync2(source));
475
612
  if (!pngHasAlpha(bytes) || process.platform !== "darwin") {
476
613
  cpSync(source, target);
477
614
  return;
@@ -480,27 +617,27 @@ function writeOpaqueIcon(source, target) {
480
617
  const run = (args) => Bun.spawnSync(["sips", ...args], { stdout: "ignore", stderr: "pipe" });
481
618
  const toJpeg = run(["-s", "format", "jpeg", "-s", "formatOptions", "100", source, "--out", flattened]);
482
619
  const toPng = toJpeg.exitCode === 0 ? run(["-s", "format", "png", flattened, "--out", target]) : toJpeg;
483
- rmSync(flattened, { force: true });
620
+ rmSync2(flattened, { force: true });
484
621
  if (toPng.exitCode !== 0) {
485
622
  cpSync(source, target);
486
623
  console.warn(`[craft] Could not remove the app icon's alpha channel; App Store Connect will refuse it: ${toPng.stderr.toString().trim()}`);
487
624
  }
488
625
  }
489
626
  function renderAssetCatalog(output, config) {
490
- const catalog = join(output, "Assets.xcassets");
491
- const appIcon = join(catalog, "AppIcon.appiconset");
492
- const launchBackground = join(catalog, "LaunchBackground.colorset");
493
- mkdirSync(appIcon, { recursive: true });
494
- mkdirSync(launchBackground, { recursive: true });
495
- writeFileSync(join(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
627
+ const catalog = join2(output, "Assets.xcassets");
628
+ const appIcon = join2(catalog, "AppIcon.appiconset");
629
+ const launchBackground = join2(catalog, "LaunchBackground.colorset");
630
+ mkdirSync2(appIcon, { recursive: true });
631
+ mkdirSync2(launchBackground, { recursive: true });
632
+ writeFileSync2(join2(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
496
633
  `);
497
634
  const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
498
635
  if (config.appIconPath) {
499
636
  if (!existsSync(config.appIconPath))
500
637
  throw new Error(`App icon not found: ${config.appIconPath}`);
501
- writeOpaqueIcon(config.appIconPath, join(appIcon, iconFilename));
638
+ writeOpaqueIcon(config.appIconPath, join2(appIcon, iconFilename));
502
639
  }
503
- writeFileSync(join(appIcon, "Contents.json"), `${JSON.stringify({
640
+ writeFileSync2(join2(appIcon, "Contents.json"), `${JSON.stringify({
504
641
  images: iconFilename ? [{
505
642
  filename: iconFilename,
506
643
  idiom: "universal",
@@ -527,7 +664,7 @@ function renderAssetCatalog(output, config) {
527
664
  idiom: "universal"
528
665
  });
529
666
  }
530
- writeFileSync(join(launchBackground, "Contents.json"), `${JSON.stringify({
667
+ writeFileSync2(join2(launchBackground, "Contents.json"), `${JSON.stringify({
531
668
  colors,
532
669
  info: { author: "xcode", version: 1 }
533
670
  }, null, 2)}
@@ -536,11 +673,11 @@ function renderAssetCatalog(output, config) {
536
673
  }
537
674
  var LAUNCH_LOGO = "LaunchLogo";
538
675
  function renderLaunchLogo(catalog, config) {
539
- const imageset = join(catalog, `${LAUNCH_LOGO}.imageset`);
540
- rmSync(imageset, { recursive: true, force: true });
676
+ const imageset = join2(catalog, `${LAUNCH_LOGO}.imageset`);
677
+ rmSync2(imageset, { recursive: true, force: true });
541
678
  if (!config.splashImagePath)
542
679
  return;
543
- mkdirSync(imageset, { recursive: true });
680
+ mkdirSync2(imageset, { recursive: true });
544
681
  const images = [];
545
682
  let vector = false;
546
683
  for (const [source, dark] of [[config.splashImagePath, false], [config.splashImagePathDark, true]]) {
@@ -552,14 +689,14 @@ function renderLaunchLogo(catalog, config) {
552
689
  if (extension === "svg" || extension === "pdf")
553
690
  vector = true;
554
691
  const filename = `${LAUNCH_LOGO}${dark ? "-dark" : ""}.${extension}`;
555
- cpSync(source, join(imageset, filename));
692
+ cpSync(source, join2(imageset, filename));
556
693
  images.push({
557
694
  ...dark ? { appearances: [{ appearance: "luminosity", value: "dark" }] } : {},
558
695
  filename,
559
696
  idiom: "universal"
560
697
  });
561
698
  }
562
- writeFileSync(join(imageset, "Contents.json"), `${JSON.stringify({
699
+ writeFileSync2(join2(imageset, "Contents.json"), `${JSON.stringify({
563
700
  images,
564
701
  info: { author: "xcode", version: 1 },
565
702
  ...vector ? { properties: { "preserves-vector-representation": true } } : {}
@@ -673,15 +810,15 @@ function syncWebAssets(source, output) {
673
810
  const sourcePath = resolve(source);
674
811
  if (!existsSync(sourcePath))
675
812
  throw new Error(`Web asset path not found: ${source}`);
676
- const distDir = join(output, "dist");
677
- rmSync(distDir, { recursive: true, force: true });
678
- mkdirSync(distDir, { recursive: true });
813
+ const distDir = join2(output, "dist");
814
+ rmSync2(distDir, { recursive: true, force: true });
815
+ mkdirSync2(distDir, { recursive: true });
679
816
  if (statSync(sourcePath).isDirectory()) {
680
817
  cpSync(sourcePath, distDir, { recursive: true });
681
818
  } else {
682
- cpSync(sourcePath, join(distDir, "index.html"));
819
+ cpSync(sourcePath, join2(distDir, "index.html"));
683
820
  }
684
- if (!existsSync(join(distDir, "index.html"))) {
821
+ if (!existsSync(join2(distDir, "index.html"))) {
685
822
  throw new Error(`Web asset directory must contain index.html: ${source}`);
686
823
  }
687
824
  }
@@ -703,32 +840,32 @@ var RUNTIME_ARCHIVES = {
703
840
  };
704
841
  async function installRuntime(output, runtimeDir) {
705
842
  const resolved = Object.entries(RUNTIME_ARCHIVES).map(([sdk, archives]) => {
706
- const present = archives.filter((a) => existsSync(join(runtimeDir, a)));
843
+ const present = archives.filter((a) => existsSync(join2(runtimeDir, a)));
707
844
  if (present.length === 0) {
708
845
  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.`);
709
846
  }
710
847
  return { sdk, archives, present };
711
848
  });
712
- const dest = join(output, "Runtime");
713
- rmSync(dest, { recursive: true, force: true });
849
+ const dest = join2(output, "Runtime");
850
+ rmSync2(dest, { recursive: true, force: true });
714
851
  for (const { sdk, archives, present } of resolved) {
715
- const sdkDir = join(dest, sdk);
716
- mkdirSync(sdkDir, { recursive: true });
717
- const target = join(sdkDir, "libcraft-ios.a");
852
+ const sdkDir = join2(dest, sdk);
853
+ mkdirSync2(sdkDir, { recursive: true });
854
+ const target = join2(sdkDir, "libcraft-ios.a");
718
855
  if (present.length === 1) {
719
856
  const missing = archives.filter((a) => !present.includes(a));
720
857
  if (missing.length > 0) {
721
858
  console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
722
859
  }
723
- cpSync(join(runtimeDir, present[0]), target);
860
+ cpSync(join2(runtimeDir, present[0]), target);
724
861
  } else {
725
- await $`lipo -create ${present.map((a) => join(runtimeDir, a))} -output ${target}`.quiet();
862
+ await $`lipo -create ${present.map((a) => join2(runtimeDir, a))} -output ${target}`.quiet();
726
863
  }
727
864
  }
728
865
  return true;
729
866
  }
730
867
  async function refreshRuntime(output, override) {
731
- if (!existsSync(join(output, "Runtime")))
868
+ if (!existsSync(join2(output, "Runtime")))
732
869
  return;
733
870
  const dir = resolveRuntimeDir(override);
734
871
  if (!dir) {
@@ -745,6 +882,43 @@ function productName(name) {
745
882
  return "Craft";
746
883
  return /^\d/.test(joined) ? `App${joined}` : joined;
747
884
  }
885
+ function restoreShell(output) {
886
+ const config = JSON.parse(readFileSync2(join2(output, "craft.config.json"), "utf8"));
887
+ writeShell(output, config);
888
+ }
889
+ function writeShell(output, config) {
890
+ if (!config.appName || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(config.bundleId))
891
+ throw new Error("The iOS shell needs an appName and a valid bundleId in craft.config.json.");
892
+ const name = productName(config.appName);
893
+ mkdirSync2(join2(output, "Sources"), { recursive: true });
894
+ mkdirSync2(join2(output, "Shared"), { recursive: true });
895
+ const swiftTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
896
+ const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, config.bundleId).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION);
897
+ writeFileSync2(join2(output, "Sources", `${name}App.swift`), swiftSource);
898
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeScreen.swift"), join2(output, "Sources", "CraftNativeScreen.swift"));
899
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join2(output, "Sources", "CraftNativeFlatList.swift"));
900
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeActions.swift"), join2(output, "Sources", "CraftNativeActions.swift"));
901
+ cpSync(join2(TEMPLATES_DIR, "CraftHealth.swift"), join2(output, "Sources", "CraftHealth.swift"));
902
+ cpSync(join2(TEMPLATES_DIR, "CraftMusic.swift"), join2(output, "Sources", "CraftMusic.swift"));
903
+ cpSync(join2(TEMPLATES_DIR, "CraftHeartRate.swift"), join2(output, "Sources", "CraftHeartRate.swift"));
904
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeMutation.swift"), join2(output, "Sources", "CraftNativeMutation.swift"));
905
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeUI.swift"), join2(output, "Sources", "CraftNativeUI.swift"));
906
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeServices.swift"), join2(output, "Sources", "CraftNativeServices.swift"));
907
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeFiles.swift"), join2(output, "Sources", "CraftNativeFiles.swift"));
908
+ cpSync(join2(TEMPLATES_DIR, "CraftWebAudio.swift"), join2(output, "Sources", "CraftWebAudio.swift"));
909
+ cpSync(join2(TEMPLATES_DIR, "CraftHybrid.swift"), join2(output, "Sources", "CraftHybrid.swift"));
910
+ cpSync(join2(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join2(output, "Shared", "CraftActivityAttributes.swift"));
911
+ for (const [enabled, folder, template, filename] of [
912
+ [config.enableLiveActivities, "WidgetExtension", "CraftLiveActivityWidget.swift.template", `${name}LiveActivity.swift`],
913
+ [config.enableWatchApp, "WatchExtension", "CraftWatchApp.swift.template", `${name}WatchApp.swift`]
914
+ ]) {
915
+ if (!enabled)
916
+ continue;
917
+ mkdirSync2(join2(output, folder), { recursive: true });
918
+ const source = readFileSync2(join2(TEMPLATES_DIR, template), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
919
+ writeFileSync2(join2(output, folder, filename), source);
920
+ }
921
+ }
748
922
  async function init(options) {
749
923
  const { name: displayName, bundleId, teamId, output } = options;
750
924
  const name = productName(displayName);
@@ -752,12 +926,12 @@ async function init(options) {
752
926
  \u26A1 Initializing Craft iOS project: ${displayName}`);
753
927
  console.log(` Output: ${output}
754
928
  `);
755
- const dirs = [output, join(output, "Sources"), join(output, "Shared"), join(output, "dist")];
929
+ const dirs = [output, join2(output, "Sources"), join2(output, "Shared"), join2(output, "dist")];
756
930
  if (options.config?.enableWatchApp)
757
- dirs.push(join(output, "WatchApp"), join(output, "WatchExtension"));
931
+ dirs.push(join2(output, "WatchApp"), join2(output, "WatchExtension"));
758
932
  for (const dir of dirs) {
759
933
  if (!existsSync(dir)) {
760
- mkdirSync(dir, { recursive: true });
934
+ mkdirSync2(dir, { recursive: true });
761
935
  }
762
936
  }
763
937
  const finalBundleId = bundleId || `com.craft.${name.toLowerCase()}`;
@@ -781,29 +955,15 @@ async function init(options) {
781
955
  `));
782
956
  if (Object.keys(config.shareStorage ?? {}).length)
783
957
  config.enableSecureStorage = true;
784
- writeFileSync(join(output, "craft.config.json"), JSON.stringify(config, null, 2));
785
- const swiftTemplate = readFileSync(join(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
786
- const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION);
787
- writeFileSync(join(output, "Sources", `${name}App.swift`), swiftSource);
788
- cpSync(join(TEMPLATES_DIR, "CraftNativeScreen.swift"), join(output, "Sources", "CraftNativeScreen.swift"));
789
- cpSync(join(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join(output, "Sources", "CraftNativeFlatList.swift"));
790
- cpSync(join(TEMPLATES_DIR, "CraftNativeActions.swift"), join(output, "Sources", "CraftNativeActions.swift"));
791
- cpSync(join(TEMPLATES_DIR, "CraftHealth.swift"), join(output, "Sources", "CraftHealth.swift"));
792
- cpSync(join(TEMPLATES_DIR, "CraftMusic.swift"), join(output, "Sources", "CraftMusic.swift"));
793
- cpSync(join(TEMPLATES_DIR, "CraftHeartRate.swift"), join(output, "Sources", "CraftHeartRate.swift"));
794
- cpSync(join(TEMPLATES_DIR, "CraftNativeMutation.swift"), join(output, "Sources", "CraftNativeMutation.swift"));
795
- cpSync(join(TEMPLATES_DIR, "CraftNativeUI.swift"), join(output, "Sources", "CraftNativeUI.swift"));
796
- cpSync(join(TEMPLATES_DIR, "CraftNativeServices.swift"), join(output, "Sources", "CraftNativeServices.swift"));
797
- cpSync(join(TEMPLATES_DIR, "CraftNativeFiles.swift"), join(output, "Sources", "CraftNativeFiles.swift"));
798
- cpSync(join(TEMPLATES_DIR, "CraftWebAudio.swift"), join(output, "Sources", "CraftWebAudio.swift"));
799
- cpSync(join(TEMPLATES_DIR, "CraftHybrid.swift"), join(output, "Sources", "CraftHybrid.swift"));
800
- const infoPlistTemplate = readFileSync(join(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
958
+ writeFileSync2(join2(output, "craft.config.json"), JSON.stringify(config, null, 2));
959
+ writeShell(output, config);
960
+ const infoPlistTemplate = readFileSync2(join2(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
801
961
  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>
802
962
  <true/>
803
963
  <key>NSSupportsLiveActivitiesFrequentUpdates</key>
804
964
  <true/>` : "");
805
- writeFileSync(join(output, "Info.plist"), infoPlist);
806
- const projectYmlTemplate = readFileSync(join(TEMPLATES_DIR, "project.yml.template"), "utf-8");
965
+ writeFileSync2(join2(output, "Info.plist"), infoPlist);
966
+ const projectYmlTemplate = readFileSync2(join2(TEMPLATES_DIR, "project.yml.template"), "utf-8");
807
967
  const nativeDependencies = [];
808
968
  if (config.enableLiveActivities)
809
969
  nativeDependencies.push(` - target: ${name}LiveActivity`);
@@ -860,35 +1020,30 @@ async function init(options) {
860
1020
  if (hasRuntime) {
861
1021
  console.log(" Linked the Zig runtime from", runtimeDir);
862
1022
  } else {
863
- rmSync(join(output, "Runtime"), { recursive: true, force: true });
1023
+ rmSync2(join2(output, "Runtime"), { recursive: true, force: true });
864
1024
  }
865
1025
  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:
866
1026
  ${nativeDependencies.join(`
867
1027
  `)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
868
1028
  `));
869
- writeFileSync(join(output, "project.yml"), projectYml);
870
- writeFileSync(join(output, "Craft.entitlements"), renderEntitlements(config));
871
- writeFileSync(join(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
1029
+ writeFileSync2(join2(output, "project.yml"), projectYml);
1030
+ writeFileSync2(join2(output, "Craft.entitlements"), renderEntitlements(config));
1031
+ writeFileSync2(join2(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
872
1032
  renderAssetCatalog(output, config);
873
- cpSync(join(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join(output, "Shared", "CraftActivityAttributes.swift"));
874
1033
  if (config.enableLiveActivities) {
875
- mkdirSync(join(output, "WidgetExtension"), { recursive: true });
876
- const widgetSource = readFileSync(join(TEMPLATES_DIR, "CraftLiveActivityWidget.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
877
- writeFileSync(join(output, "WidgetExtension", `${name}LiveActivity.swift`), widgetSource);
878
- const widgetInfo = readFileSync(join(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
879
- writeFileSync(join(output, "WidgetExtension", "Info.plist"), widgetInfo);
1034
+ mkdirSync2(join2(output, "WidgetExtension"), { recursive: true });
1035
+ const widgetInfo = readFileSync2(join2(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
1036
+ writeFileSync2(join2(output, "WidgetExtension", "Info.plist"), widgetInfo);
880
1037
  }
881
1038
  if (config.enableWatchApp) {
882
- const watchSource = readFileSync(join(TEMPLATES_DIR, "CraftWatchApp.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
883
- writeFileSync(join(output, "WatchExtension", `${name}WatchApp.swift`), watchSource);
884
- const watchInfo = readFileSync(join(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
885
- writeFileSync(join(output, "WatchApp", "Info.plist"), watchInfo);
886
- writeFileSync(join(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
887
- const watchExtensionInfo = readFileSync(join(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
888
- writeFileSync(join(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
889
- writeFileSync(join(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
1039
+ const watchInfo = readFileSync2(join2(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1040
+ writeFileSync2(join2(output, "WatchApp", "Info.plist"), watchInfo);
1041
+ writeFileSync2(join2(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
1042
+ const watchExtensionInfo = readFileSync2(join2(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1043
+ writeFileSync2(join2(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
1044
+ writeFileSync2(join2(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
890
1045
  }
891
- writeFileSync(join(output, "dist", "index.html"), renderOfflinePage(config));
1046
+ writeFileSync2(join2(output, "dist", "index.html"), renderOfflinePage(config));
892
1047
  if (isHybrid(config)) {
893
1048
  if (config.nativeBundle)
894
1049
  installNativeBundle(config.nativeBundle, output);
@@ -913,11 +1068,11 @@ async function build(options) {
913
1068
  const { htmlPath, nativeBundlePath, devServer, output, generateProject = true } = options;
914
1069
  console.log(`
915
1070
  \uD83D\uDCE6 Building Craft iOS project...`);
916
- const configPath = join(output, "craft.config.json");
1071
+ const configPath = join2(output, "craft.config.json");
917
1072
  if (!existsSync(configPath)) {
918
1073
  throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
919
1074
  }
920
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
1075
+ const config = JSON.parse(readFileSync2(configPath, "utf-8"));
921
1076
  if (config.renderer !== undefined && config.renderer !== "web" && config.renderer !== "native") {
922
1077
  throw new Error(`Unknown iOS renderer: ${config.renderer}. Expected web or native.`);
923
1078
  }
@@ -926,11 +1081,11 @@ async function build(options) {
926
1081
  throw new Error("A native iOS screen cannot use --html-path or --dev-server.");
927
1082
  }
928
1083
  if (nativeBundlePath) {
929
- const destination = join(output, "dist", "native-screen.js");
1084
+ const destination = join2(output, "dist", "native-screen.js");
930
1085
  if (resolve(nativeBundlePath) !== resolve(destination))
931
1086
  cpSync(nativeBundlePath, destination);
932
1087
  }
933
- if (!existsSync(join(output, "dist", "native-screen.js"))) {
1088
+ if (!existsSync(join2(output, "dist", "native-screen.js"))) {
934
1089
  throw new Error("Native iOS mode needs dist/native-screen.js. Compile a .stx screen with `stx native compile` first.");
935
1090
  }
936
1091
  } else if (nativeBundlePath && !isHybrid(config)) {
@@ -944,7 +1099,7 @@ async function build(options) {
944
1099
  config.devServerURL = devServer;
945
1100
  const origin = new URL(devServer).origin;
946
1101
  config.trustedOrigins = [...new Set([...config.trustedOrigins ?? [], origin])];
947
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1102
+ writeFileSync2(configPath, JSON.stringify(config, null, 2));
948
1103
  console.log(` Dev server: ${devServer}`);
949
1104
  }
950
1105
  if (htmlPath) {
@@ -956,7 +1111,7 @@ async function build(options) {
956
1111
  if (bundle) {
957
1112
  installNativeBundle(bundle, output);
958
1113
  console.log(` Native screens: ${bundle} \u2192 dist/native-screen.js`);
959
- } else if (!existsSync(join(output, "dist", "native-screen.js"))) {
1114
+ } else if (!existsSync(join2(output, "dist", "native-screen.js"))) {
960
1115
  console.warn(" \u26A0 nativeScreens is set but there is no native bundle: every path opens in the web view.");
961
1116
  }
962
1117
  }
@@ -986,7 +1141,7 @@ async function open(options) {
986
1141
  if (!xcodeproj) {
987
1142
  throw new Error(`No Xcode project found in ${output}. Run 'craft ios build' first.`);
988
1143
  }
989
- const projectPath = join(output, xcodeproj);
1144
+ const projectPath = join2(output, xcodeproj);
990
1145
  console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
991
1146
  await $`open ${projectPath}`;
992
1147
  }
@@ -1027,7 +1182,7 @@ async function showSimulator() {
1027
1182
  const selected = await $`xcode-select -p`.quiet().nothrow();
1028
1183
  const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
1029
1184
  if (developer) {
1030
- const app = join(developer, "Applications", "Simulator.app");
1185
+ const app = join2(developer, "Applications", "Simulator.app");
1031
1186
  if (existsSync(app)) {
1032
1187
  await $`open ${app}`.quiet().nothrow();
1033
1188
  return;
@@ -1043,12 +1198,12 @@ async function run(options) {
1043
1198
  if (!xcodeproj) {
1044
1199
  throw new Error(`No Xcode project found in ${output}`);
1045
1200
  }
1046
- const projectPath = join(output, xcodeproj);
1201
+ const projectPath = join2(output, xcodeproj);
1047
1202
  const appName = xcodeproj.replace(".xcodeproj", "");
1048
- const configPath = join(output, "craft.config.json");
1203
+ const configPath = join2(output, "craft.config.json");
1049
1204
  if (!existsSync(configPath))
1050
1205
  throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
1051
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
1206
+ const config = JSON.parse(readFileSync2(configPath, "utf-8"));
1052
1207
  if (simulator) {
1053
1208
  console.log("\uD83D\uDCF1 Building and running on simulator...");
1054
1209
  const device = await pickSimulator();
@@ -1058,8 +1213,8 @@ async function run(options) {
1058
1213
  console.log(` Device: ${device.name} (${device.runtime})`);
1059
1214
  try {
1060
1215
  await bootSimulator(device);
1061
- const derivedData = join(output, "build");
1062
- const product = join(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
1216
+ const derivedData = join2(output, "build");
1217
+ const product = join2(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
1063
1218
  await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
1064
1219
  if (!existsSync(product)) {
1065
1220
  throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
@@ -1085,7 +1240,9 @@ async function run(options) {
1085
1240
  export {
1086
1241
  syncWebAssets,
1087
1242
  showSimulator,
1243
+ serializeSimulatorSteps,
1088
1244
  run,
1245
+ restoreShell,
1089
1246
  resolveRuntimeDir,
1090
1247
  resolveAppearance,
1091
1248
  renderWatchEntitlements,
@@ -1112,8 +1269,10 @@ export {
1112
1269
  matchNativePath,
1113
1270
  isHybrid,
1114
1271
  installRuntime,
1272
+ initSimulatorDriver,
1115
1273
  init,
1116
1274
  hybridConfigProblems,
1275
+ createNativeMutationValidator,
1117
1276
  build,
1118
1277
  bootSimulator,
1119
1278
  backgroundRefreshIdentifier,
@@ -0,0 +1,71 @@
1
+ export interface SimulatorDriverOptions {
2
+ output: string;
3
+ /** The installed app to drive; the test host is a separate application. */
4
+ bundleId: string;
5
+ projectName?: string;
6
+ bundleIdPrefix?: string;
7
+ deploymentTarget?: string;
8
+ controlUrl?: string;
9
+ tabs?: readonly string[];
10
+ }
11
+ /** Generate the reusable XCUITest shell from package templates. No app-owned Swift. */
12
+ export declare function initSimulatorDriver(options: SimulatorDriverOptions): void;
13
+ export type SimulatorStep = {
14
+ command: 'launch' | 'activate' | 'terminate' | 'home' | 'back' | 'return' | 'unlabelled';
15
+ } | {
16
+ command: 'background' | 'wait';
17
+ seconds: number;
18
+ } | {
19
+ command: 'delay';
20
+ milliseconds: number;
21
+ } | {
22
+ command: 'net';
23
+ state: 'online' | 'offline';
24
+ } | {
25
+ command: 'swipe';
26
+ direction: 'up' | 'down' | 'left' | 'right';
27
+ } | {
28
+ command: 'tabbar';
29
+ state: 'shown' | 'hidden';
30
+ } | {
31
+ command: 'tapxy';
32
+ x: number;
33
+ y: number;
34
+ } | {
35
+ command: 'field';
36
+ index: number;
37
+ text: string;
38
+ } | {
39
+ command: 'typeid';
40
+ id: string;
41
+ text: string;
42
+ } | {
43
+ command: 'expect' | 'expectexact' | 'expectid';
44
+ text: string;
45
+ seconds?: number;
46
+ } | {
47
+ command: 'tap' | 'tapif' | 'tapid' | 'tapexact' | 'type' | 'absent' | 'absentid' | 'selected' | 'tab' | 'scrollto' | 'dismiss' | 'log';
48
+ text: string;
49
+ } | {
50
+ command: 'shot' | 'tree' | 'audit' | 'exec';
51
+ name: string;
52
+ } | {
53
+ command: 'openurl';
54
+ url: string;
55
+ };
56
+ /** Typed journeys use the same wire protocol as existing line-based scripts. */
57
+ export declare function serializeSimulatorSteps(steps: readonly SimulatorStep[]): string;
58
+ export interface NativeValidationResult {
59
+ exitCode: number;
60
+ stdout: string;
61
+ stderr: string;
62
+ }
63
+ export interface NativeMutationValidator {
64
+ validate(messages: readonly {
65
+ type: string;
66
+ payload?: unknown;
67
+ }[]): NativeValidationResult;
68
+ dispose(): void;
69
+ }
70
+ /** Compile the actual Foundation mutation host once, then validate captured STX messages. Requires macOS and swiftc. */
71
+ export declare function createNativeMutationValidator(): NativeMutationValidator;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-native/ios",
3
- "version": "0.0.153",
3
+ "version": "0.0.155",
4
4
  "homepage": "https://github.com/craft-native/craft/tree/main/packages/ios#readme",
5
5
  "repository": {
6
6
  "type": "git",