craft-native 0.0.154 → 0.0.156

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.
@@ -1,12 +1,149 @@
1
1
  // @bun
2
2
  // ../ios/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
+ // ../ios/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
+ // ../ios/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
  }
@@ -91,11 +228,25 @@ function matchNativePath(nativeScreens, raw) {
91
228
  function isHybrid(config) {
92
229
  return (config.renderer ?? "web") === "web" && Object.keys(config.nativeScreens ?? {}).length > 0;
93
230
  }
231
+ function hasNativeRoutes(config) {
232
+ return config.renderer === "native" && Object.keys(config.nativeScreens ?? {}).length > 0;
233
+ }
234
+ function usesNativeBundle(config) {
235
+ return config.renderer === "native" || Object.keys(config.nativeScreens ?? {}).length > 0;
236
+ }
237
+ function nativeStartPath(config) {
238
+ if (!config.devServerURL)
239
+ return "/";
240
+ try {
241
+ const url = new URL(config.devServerURL);
242
+ return `${url.pathname || "/"}${url.search}`;
243
+ } catch {
244
+ return "/";
245
+ }
246
+ }
94
247
  function hybridConfigProblems(config) {
95
248
  const problems = [];
96
249
  const screens = Object.entries(config.nativeScreens ?? {});
97
- if (screens.length && config.renderer === "native")
98
- problems.push('nativeScreens mixes native screens into a web app; it needs renderer: "web" (renderer: "native" is all native already).');
99
250
  for (const [pattern, screen] of screens) {
100
251
  if (normalizeNativePath(pattern) !== pattern || /[?#]/.test(pattern))
101
252
  problems.push(`nativeScreens path "${pattern}" must be an absolute path with no query, hash or trailing slash.`);
@@ -117,13 +268,28 @@ function hybridConfigProblems(config) {
117
268
  if (!key || typeof target !== "string" || !target)
118
269
  problems.push(`shareStorage "${key}" needs the Keychain key native screens read it under.`);
119
270
  }
271
+ if (hasNativeRoutes(config)) {
272
+ for (const id of ids) {
273
+ if (!matchNativePath(config.nativeScreens, id))
274
+ problems.push(`Tab "${id}" is not a nativeScreens path; with renderer: "native" every tab must be a native screen.`);
275
+ }
276
+ const start = nativeStartPath(config);
277
+ if (!matchNativePath(config.nativeScreens, start))
278
+ problems.push(`The start path "${start}" (from devServerURL) is not a nativeScreens path; with renderer: "native" the app must start on a native screen.`);
279
+ }
280
+ if (config.nativeApp !== undefined) {
281
+ if (typeof config.nativeApp !== "string" || !/^[A-Za-z_$][\w$-]*$/.test(config.nativeApp))
282
+ problems.push(`nativeApp "${String(config.nativeApp)}" must be a route name from the native bundle.`);
283
+ else if (!usesNativeBundle(config))
284
+ problems.push('nativeApp runs in the native bundle; it needs nativeScreens or renderer: "native".');
285
+ }
120
286
  return problems;
121
287
  }
122
288
  function installNativeBundle(source, output) {
123
289
  if (!existsSync(source))
124
290
  throw new Error(`Native bundle not found: ${source}`);
125
- const destination = join(output, "dist", "native-screen.js");
126
- mkdirSync(dirname(destination), { recursive: true });
291
+ const destination = join2(output, "dist", "native-screen.js");
292
+ mkdirSync2(dirname2(destination), { recursive: true });
127
293
  if (resolve(source) !== resolve(destination))
128
294
  cpSync(source, destination);
129
295
  }
@@ -471,7 +637,7 @@ function pngHasAlpha(bytes) {
471
637
  return false;
472
638
  }
473
639
  function writeOpaqueIcon(source, target) {
474
- const bytes = new Uint8Array(readFileSync(source));
640
+ const bytes = new Uint8Array(readFileSync2(source));
475
641
  if (!pngHasAlpha(bytes) || process.platform !== "darwin") {
476
642
  cpSync(source, target);
477
643
  return;
@@ -480,27 +646,27 @@ function writeOpaqueIcon(source, target) {
480
646
  const run = (args) => Bun.spawnSync(["sips", ...args], { stdout: "ignore", stderr: "pipe" });
481
647
  const toJpeg = run(["-s", "format", "jpeg", "-s", "formatOptions", "100", source, "--out", flattened]);
482
648
  const toPng = toJpeg.exitCode === 0 ? run(["-s", "format", "png", flattened, "--out", target]) : toJpeg;
483
- rmSync(flattened, { force: true });
649
+ rmSync2(flattened, { force: true });
484
650
  if (toPng.exitCode !== 0) {
485
651
  cpSync(source, target);
486
652
  console.warn(`[craft] Could not remove the app icon's alpha channel; App Store Connect will refuse it: ${toPng.stderr.toString().trim()}`);
487
653
  }
488
654
  }
489
655
  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)}
656
+ const catalog = join2(output, "Assets.xcassets");
657
+ const appIcon = join2(catalog, "AppIcon.appiconset");
658
+ const launchBackground = join2(catalog, "LaunchBackground.colorset");
659
+ mkdirSync2(appIcon, { recursive: true });
660
+ mkdirSync2(launchBackground, { recursive: true });
661
+ writeFileSync2(join2(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
496
662
  `);
497
663
  const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
498
664
  if (config.appIconPath) {
499
665
  if (!existsSync(config.appIconPath))
500
666
  throw new Error(`App icon not found: ${config.appIconPath}`);
501
- writeOpaqueIcon(config.appIconPath, join(appIcon, iconFilename));
667
+ writeOpaqueIcon(config.appIconPath, join2(appIcon, iconFilename));
502
668
  }
503
- writeFileSync(join(appIcon, "Contents.json"), `${JSON.stringify({
669
+ writeFileSync2(join2(appIcon, "Contents.json"), `${JSON.stringify({
504
670
  images: iconFilename ? [{
505
671
  filename: iconFilename,
506
672
  idiom: "universal",
@@ -527,7 +693,7 @@ function renderAssetCatalog(output, config) {
527
693
  idiom: "universal"
528
694
  });
529
695
  }
530
- writeFileSync(join(launchBackground, "Contents.json"), `${JSON.stringify({
696
+ writeFileSync2(join2(launchBackground, "Contents.json"), `${JSON.stringify({
531
697
  colors,
532
698
  info: { author: "xcode", version: 1 }
533
699
  }, null, 2)}
@@ -536,11 +702,11 @@ function renderAssetCatalog(output, config) {
536
702
  }
537
703
  var LAUNCH_LOGO = "LaunchLogo";
538
704
  function renderLaunchLogo(catalog, config) {
539
- const imageset = join(catalog, `${LAUNCH_LOGO}.imageset`);
540
- rmSync(imageset, { recursive: true, force: true });
705
+ const imageset = join2(catalog, `${LAUNCH_LOGO}.imageset`);
706
+ rmSync2(imageset, { recursive: true, force: true });
541
707
  if (!config.splashImagePath)
542
708
  return;
543
- mkdirSync(imageset, { recursive: true });
709
+ mkdirSync2(imageset, { recursive: true });
544
710
  const images = [];
545
711
  let vector = false;
546
712
  for (const [source, dark] of [[config.splashImagePath, false], [config.splashImagePathDark, true]]) {
@@ -552,14 +718,14 @@ function renderLaunchLogo(catalog, config) {
552
718
  if (extension === "svg" || extension === "pdf")
553
719
  vector = true;
554
720
  const filename = `${LAUNCH_LOGO}${dark ? "-dark" : ""}.${extension}`;
555
- cpSync(source, join(imageset, filename));
721
+ cpSync(source, join2(imageset, filename));
556
722
  images.push({
557
723
  ...dark ? { appearances: [{ appearance: "luminosity", value: "dark" }] } : {},
558
724
  filename,
559
725
  idiom: "universal"
560
726
  });
561
727
  }
562
- writeFileSync(join(imageset, "Contents.json"), `${JSON.stringify({
728
+ writeFileSync2(join2(imageset, "Contents.json"), `${JSON.stringify({
563
729
  images,
564
730
  info: { author: "xcode", version: 1 },
565
731
  ...vector ? { properties: { "preserves-vector-representation": true } } : {}
@@ -673,15 +839,15 @@ function syncWebAssets(source, output) {
673
839
  const sourcePath = resolve(source);
674
840
  if (!existsSync(sourcePath))
675
841
  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 });
842
+ const distDir = join2(output, "dist");
843
+ rmSync2(distDir, { recursive: true, force: true });
844
+ mkdirSync2(distDir, { recursive: true });
679
845
  if (statSync(sourcePath).isDirectory()) {
680
846
  cpSync(sourcePath, distDir, { recursive: true });
681
847
  } else {
682
- cpSync(sourcePath, join(distDir, "index.html"));
848
+ cpSync(sourcePath, join2(distDir, "index.html"));
683
849
  }
684
- if (!existsSync(join(distDir, "index.html"))) {
850
+ if (!existsSync(join2(distDir, "index.html"))) {
685
851
  throw new Error(`Web asset directory must contain index.html: ${source}`);
686
852
  }
687
853
  }
@@ -703,32 +869,32 @@ var RUNTIME_ARCHIVES = {
703
869
  };
704
870
  async function installRuntime(output, runtimeDir) {
705
871
  const resolved = Object.entries(RUNTIME_ARCHIVES).map(([sdk, archives]) => {
706
- const present = archives.filter((a) => existsSync(join(runtimeDir, a)));
872
+ const present = archives.filter((a) => existsSync(join2(runtimeDir, a)));
707
873
  if (present.length === 0) {
708
874
  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
875
  }
710
876
  return { sdk, archives, present };
711
877
  });
712
- const dest = join(output, "Runtime");
713
- rmSync(dest, { recursive: true, force: true });
878
+ const dest = join2(output, "Runtime");
879
+ rmSync2(dest, { recursive: true, force: true });
714
880
  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");
881
+ const sdkDir = join2(dest, sdk);
882
+ mkdirSync2(sdkDir, { recursive: true });
883
+ const target = join2(sdkDir, "libcraft-ios.a");
718
884
  if (present.length === 1) {
719
885
  const missing = archives.filter((a) => !present.includes(a));
720
886
  if (missing.length > 0) {
721
887
  console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
722
888
  }
723
- cpSync(join(runtimeDir, present[0]), target);
889
+ cpSync(join2(runtimeDir, present[0]), target);
724
890
  } else {
725
- await $`lipo -create ${present.map((a) => join(runtimeDir, a))} -output ${target}`.quiet();
891
+ await $`lipo -create ${present.map((a) => join2(runtimeDir, a))} -output ${target}`.quiet();
726
892
  }
727
893
  }
728
894
  return true;
729
895
  }
730
896
  async function refreshRuntime(output, override) {
731
- if (!existsSync(join(output, "Runtime")))
897
+ if (!existsSync(join2(output, "Runtime")))
732
898
  return;
733
899
  const dir = resolveRuntimeDir(override);
734
900
  if (!dir) {
@@ -745,6 +911,43 @@ function productName(name) {
745
911
  return "Craft";
746
912
  return /^\d/.test(joined) ? `App${joined}` : joined;
747
913
  }
914
+ function restoreShell(output) {
915
+ const config = JSON.parse(readFileSync2(join2(output, "craft.config.json"), "utf8"));
916
+ writeShell(output, config);
917
+ }
918
+ function writeShell(output, config) {
919
+ if (!config.appName || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(config.bundleId))
920
+ throw new Error("The iOS shell needs an appName and a valid bundleId in craft.config.json.");
921
+ const name = productName(config.appName);
922
+ mkdirSync2(join2(output, "Sources"), { recursive: true });
923
+ mkdirSync2(join2(output, "Shared"), { recursive: true });
924
+ const swiftTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
925
+ const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, config.bundleId).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION);
926
+ writeFileSync2(join2(output, "Sources", `${name}App.swift`), swiftSource);
927
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeScreen.swift"), join2(output, "Sources", "CraftNativeScreen.swift"));
928
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join2(output, "Sources", "CraftNativeFlatList.swift"));
929
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeActions.swift"), join2(output, "Sources", "CraftNativeActions.swift"));
930
+ cpSync(join2(TEMPLATES_DIR, "CraftHealth.swift"), join2(output, "Sources", "CraftHealth.swift"));
931
+ cpSync(join2(TEMPLATES_DIR, "CraftMusic.swift"), join2(output, "Sources", "CraftMusic.swift"));
932
+ cpSync(join2(TEMPLATES_DIR, "CraftHeartRate.swift"), join2(output, "Sources", "CraftHeartRate.swift"));
933
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeMutation.swift"), join2(output, "Sources", "CraftNativeMutation.swift"));
934
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeUI.swift"), join2(output, "Sources", "CraftNativeUI.swift"));
935
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeServices.swift"), join2(output, "Sources", "CraftNativeServices.swift"));
936
+ cpSync(join2(TEMPLATES_DIR, "CraftNativeFiles.swift"), join2(output, "Sources", "CraftNativeFiles.swift"));
937
+ cpSync(join2(TEMPLATES_DIR, "CraftWebAudio.swift"), join2(output, "Sources", "CraftWebAudio.swift"));
938
+ cpSync(join2(TEMPLATES_DIR, "CraftHybrid.swift"), join2(output, "Sources", "CraftHybrid.swift"));
939
+ cpSync(join2(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join2(output, "Shared", "CraftActivityAttributes.swift"));
940
+ for (const [enabled, folder, template, filename] of [
941
+ [config.enableLiveActivities, "WidgetExtension", "CraftLiveActivityWidget.swift.template", `${name}LiveActivity.swift`],
942
+ [config.enableWatchApp, "WatchExtension", "CraftWatchApp.swift.template", `${name}WatchApp.swift`]
943
+ ]) {
944
+ if (!enabled)
945
+ continue;
946
+ mkdirSync2(join2(output, folder), { recursive: true });
947
+ const source = readFileSync2(join2(TEMPLATES_DIR, template), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
948
+ writeFileSync2(join2(output, folder, filename), source);
949
+ }
950
+ }
748
951
  async function init(options) {
749
952
  const { name: displayName, bundleId, teamId, output } = options;
750
953
  const name = productName(displayName);
@@ -752,12 +955,12 @@ async function init(options) {
752
955
  \u26A1 Initializing Craft iOS project: ${displayName}`);
753
956
  console.log(` Output: ${output}
754
957
  `);
755
- const dirs = [output, join(output, "Sources"), join(output, "Shared"), join(output, "dist")];
958
+ const dirs = [output, join2(output, "Sources"), join2(output, "Shared"), join2(output, "dist")];
756
959
  if (options.config?.enableWatchApp)
757
- dirs.push(join(output, "WatchApp"), join(output, "WatchExtension"));
960
+ dirs.push(join2(output, "WatchApp"), join2(output, "WatchExtension"));
758
961
  for (const dir of dirs) {
759
962
  if (!existsSync(dir)) {
760
- mkdirSync(dir, { recursive: true });
963
+ mkdirSync2(dir, { recursive: true });
761
964
  }
762
965
  }
763
966
  const finalBundleId = bundleId || `com.craft.${name.toLowerCase()}`;
@@ -781,29 +984,15 @@ async function init(options) {
781
984
  `));
782
985
  if (Object.keys(config.shareStorage ?? {}).length)
783
986
  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");
987
+ writeFileSync2(join2(output, "craft.config.json"), JSON.stringify(config, null, 2));
988
+ writeShell(output, config);
989
+ const infoPlistTemplate = readFileSync2(join2(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
801
990
  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
991
  <true/>
803
992
  <key>NSSupportsLiveActivitiesFrequentUpdates</key>
804
993
  <true/>` : "");
805
- writeFileSync(join(output, "Info.plist"), infoPlist);
806
- const projectYmlTemplate = readFileSync(join(TEMPLATES_DIR, "project.yml.template"), "utf-8");
994
+ writeFileSync2(join2(output, "Info.plist"), infoPlist);
995
+ const projectYmlTemplate = readFileSync2(join2(TEMPLATES_DIR, "project.yml.template"), "utf-8");
807
996
  const nativeDependencies = [];
808
997
  if (config.enableLiveActivities)
809
998
  nativeDependencies.push(` - target: ${name}LiveActivity`);
@@ -860,40 +1049,37 @@ async function init(options) {
860
1049
  if (hasRuntime) {
861
1050
  console.log(" Linked the Zig runtime from", runtimeDir);
862
1051
  } else {
863
- rmSync(join(output, "Runtime"), { recursive: true, force: true });
1052
+ rmSync2(join2(output, "Runtime"), { recursive: true, force: true });
864
1053
  }
865
1054
  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
1055
  ${nativeDependencies.join(`
867
1056
  `)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
868
1057
  `));
869
- writeFileSync(join(output, "project.yml"), projectYml);
870
- writeFileSync(join(output, "Craft.entitlements"), renderEntitlements(config));
871
- writeFileSync(join(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
1058
+ writeFileSync2(join2(output, "project.yml"), projectYml);
1059
+ writeFileSync2(join2(output, "Craft.entitlements"), renderEntitlements(config));
1060
+ writeFileSync2(join2(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
872
1061
  renderAssetCatalog(output, config);
873
- cpSync(join(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join(output, "Shared", "CraftActivityAttributes.swift"));
874
1062
  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);
1063
+ mkdirSync2(join2(output, "WidgetExtension"), { recursive: true });
1064
+ const widgetInfo = readFileSync2(join2(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
1065
+ writeFileSync2(join2(output, "WidgetExtension", "Info.plist"), widgetInfo);
880
1066
  }
881
1067
  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));
1068
+ const watchInfo = readFileSync2(join2(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1069
+ writeFileSync2(join2(output, "WatchApp", "Info.plist"), watchInfo);
1070
+ writeFileSync2(join2(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
1071
+ const watchExtensionInfo = readFileSync2(join2(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1072
+ writeFileSync2(join2(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
1073
+ writeFileSync2(join2(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
890
1074
  }
891
- writeFileSync(join(output, "dist", "index.html"), renderOfflinePage(config));
892
- if (isHybrid(config)) {
1075
+ writeFileSync2(join2(output, "dist", "index.html"), renderOfflinePage(config));
1076
+ if (usesNativeBundle(config)) {
893
1077
  if (config.nativeBundle)
894
1078
  installNativeBundle(config.nativeBundle, output);
895
- else
1079
+ else if (isHybrid(config))
896
1080
  console.warn(" \u26A0 nativeScreens is set without nativeBundle: every path opens in the web view until `craft ios build --native-bundle` supplies one.");
1081
+ else if (hasNativeRoutes(config))
1082
+ console.warn(" \u26A0 nativeScreens is set without nativeBundle: the app has no screens until `craft ios build --native-bundle` supplies one.");
897
1083
  }
898
1084
  console.log("\u2705 Project initialized");
899
1085
  console.log("");
@@ -913,11 +1099,11 @@ async function build(options) {
913
1099
  const { htmlPath, nativeBundlePath, devServer, output, generateProject = true } = options;
914
1100
  console.log(`
915
1101
  \uD83D\uDCE6 Building Craft iOS project...`);
916
- const configPath = join(output, "craft.config.json");
1102
+ const configPath = join2(output, "craft.config.json");
917
1103
  if (!existsSync(configPath)) {
918
1104
  throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
919
1105
  }
920
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
1106
+ const config = JSON.parse(readFileSync2(configPath, "utf-8"));
921
1107
  if (config.renderer !== undefined && config.renderer !== "web" && config.renderer !== "native") {
922
1108
  throw new Error(`Unknown iOS renderer: ${config.renderer}. Expected web or native.`);
923
1109
  }
@@ -925,12 +1111,10 @@ async function build(options) {
925
1111
  if (htmlPath || devServer) {
926
1112
  throw new Error("A native iOS screen cannot use --html-path or --dev-server.");
927
1113
  }
928
- if (nativeBundlePath) {
929
- const destination = join(output, "dist", "native-screen.js");
930
- if (resolve(nativeBundlePath) !== resolve(destination))
931
- cpSync(nativeBundlePath, destination);
932
- }
933
- if (!existsSync(join(output, "dist", "native-screen.js"))) {
1114
+ const bundle = nativeBundlePath ?? config.nativeBundle;
1115
+ if (bundle)
1116
+ installNativeBundle(bundle, output);
1117
+ if (!existsSync(join2(output, "dist", "native-screen.js"))) {
934
1118
  throw new Error("Native iOS mode needs dist/native-screen.js. Compile a .stx screen with `stx native compile` first.");
935
1119
  }
936
1120
  } else if (nativeBundlePath && !isHybrid(config)) {
@@ -944,7 +1128,7 @@ async function build(options) {
944
1128
  config.devServerURL = devServer;
945
1129
  const origin = new URL(devServer).origin;
946
1130
  config.trustedOrigins = [...new Set([...config.trustedOrigins ?? [], origin])];
947
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1131
+ writeFileSync2(configPath, JSON.stringify(config, null, 2));
948
1132
  console.log(` Dev server: ${devServer}`);
949
1133
  }
950
1134
  if (htmlPath) {
@@ -956,7 +1140,7 @@ async function build(options) {
956
1140
  if (bundle) {
957
1141
  installNativeBundle(bundle, output);
958
1142
  console.log(` Native screens: ${bundle} \u2192 dist/native-screen.js`);
959
- } else if (!existsSync(join(output, "dist", "native-screen.js"))) {
1143
+ } else if (!existsSync(join2(output, "dist", "native-screen.js"))) {
960
1144
  console.warn(" \u26A0 nativeScreens is set but there is no native bundle: every path opens in the web view.");
961
1145
  }
962
1146
  }
@@ -986,7 +1170,7 @@ async function open(options) {
986
1170
  if (!xcodeproj) {
987
1171
  throw new Error(`No Xcode project found in ${output}. Run 'craft ios build' first.`);
988
1172
  }
989
- const projectPath = join(output, xcodeproj);
1173
+ const projectPath = join2(output, xcodeproj);
990
1174
  console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
991
1175
  await $`open ${projectPath}`;
992
1176
  }
@@ -1027,7 +1211,7 @@ async function showSimulator() {
1027
1211
  const selected = await $`xcode-select -p`.quiet().nothrow();
1028
1212
  const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
1029
1213
  if (developer) {
1030
- const app = join(developer, "Applications", "Simulator.app");
1214
+ const app = join2(developer, "Applications", "Simulator.app");
1031
1215
  if (existsSync(app)) {
1032
1216
  await $`open ${app}`.quiet().nothrow();
1033
1217
  return;
@@ -1043,12 +1227,12 @@ async function run(options) {
1043
1227
  if (!xcodeproj) {
1044
1228
  throw new Error(`No Xcode project found in ${output}`);
1045
1229
  }
1046
- const projectPath = join(output, xcodeproj);
1230
+ const projectPath = join2(output, xcodeproj);
1047
1231
  const appName = xcodeproj.replace(".xcodeproj", "");
1048
- const configPath = join(output, "craft.config.json");
1232
+ const configPath = join2(output, "craft.config.json");
1049
1233
  if (!existsSync(configPath))
1050
1234
  throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
1051
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
1235
+ const config = JSON.parse(readFileSync2(configPath, "utf-8"));
1052
1236
  if (simulator) {
1053
1237
  console.log("\uD83D\uDCF1 Building and running on simulator...");
1054
1238
  const device = await pickSimulator();
@@ -1058,8 +1242,8 @@ async function run(options) {
1058
1242
  console.log(` Device: ${device.name} (${device.runtime})`);
1059
1243
  try {
1060
1244
  await bootSimulator(device);
1061
- const derivedData = join(output, "build");
1062
- const product = join(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
1245
+ const derivedData = join2(output, "build");
1246
+ const product = join2(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
1063
1247
  await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
1064
1248
  if (!existsSync(product)) {
1065
1249
  throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
@@ -1083,9 +1267,12 @@ async function run(options) {
1083
1267
  }
1084
1268
  }
1085
1269
  export {
1270
+ usesNativeBundle,
1086
1271
  syncWebAssets,
1087
1272
  showSimulator,
1273
+ serializeSimulatorSteps,
1088
1274
  run,
1275
+ restoreShell,
1089
1276
  resolveRuntimeDir,
1090
1277
  resolveAppearance,
1091
1278
  renderWatchEntitlements,
@@ -1109,11 +1296,15 @@ export {
1109
1296
  orderSimulators,
1110
1297
  open,
1111
1298
  normalizeNativePath,
1299
+ nativeStartPath,
1112
1300
  matchNativePath,
1113
1301
  isHybrid,
1114
1302
  installRuntime,
1303
+ initSimulatorDriver,
1115
1304
  init,
1116
1305
  hybridConfigProblems,
1306
+ hasNativeRoutes,
1307
+ createNativeMutationValidator,
1117
1308
  build,
1118
1309
  bootSimulator,
1119
1310
  backgroundRefreshIdentifier,