@craft-native/ios 0.0.164 → 0.0.167

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
@@ -153,6 +153,12 @@ reversing each line's main-axis children, matching Android's native host.
153
153
 
154
154
  For multiple native screens, configure named `.stx` routes and an initial screen in `native.config.json` (see [`fixtures/native-navigation`](./fixtures/native-navigation)). Compile without a positional file: `stx native compile --format bundle --output ./screen.js`. A screen can call `craft.navigation.push('details', { id: 7 })`, `.replace('home')`, or `.back()`; the destination reads `craft.route.params`. Craft uses a native navigation stack, so the back button and edge-swipe reveal the existing controller and retain its JavaScript and input state. `bun packages/ios/scripts/test-native-navigation.ts` compiles the fixture and runs its simulator test; the default WebView renderer is unchanged.
155
155
 
156
+ Hybrid apps also get the shell's completed left/right tab swipe over the
157
+ WebView. Disable that page-wide gesture with
158
+ `craft.tabBar.set(tabs, { swipeEnabled: false })`; a native screen can still
159
+ use `craft.navigation.setOptions({ tabSwipeEnabled: false })` for its own
160
+ content.
161
+
156
162
  The optional Liquid Glass tab-bar styling is compiler-gated for Xcode 26/Swift 6.2 and later. Older SDKs compile the same tab bar with the material fallback, so generated projects remain buildable on Xcode 16.
157
163
 
158
164
  The iOS host overlays `craft.config.json` on its defaults. Missing or `null` capability flags stay disabled; explicitly set a flag to `true` to enable it. Both the Swift host and Zig bridge reject malformed values and fall back to all capabilities disabled.
@@ -424,6 +430,23 @@ await health.requestAuthorization(['mindful'], { write: true })
424
430
  await health.saveMindfulSession({ id: 'session-42', startDate: started, endDate: Date.now() })
425
431
  // Every app's mindful sessions, oldest first; ownApp marks the ones this app wrote.
426
432
  const mindful = await health.getMindfulSessions({ startDate: Date.now() - 7 * 86400000 })
433
+ // Heart rate as Apple Health receives it, instead of polling getHeartRateSamples:
434
+ // the samples since `since` (two minutes ago by default) first, then each new
435
+ // batch, oldest first and each sample once, in getHeartRateSamples' shape.
436
+ // Needs the 'heartRate' grant; one stream at a time.
437
+ const stopListening = health.onHeartRate(samples => show(samples[samples.length - 1].bpm))
438
+ await health.startHeartRateUpdates({ since: Date.now() - 60000 })
439
+ // ...
440
+ await health.stopHeartRateUpdates()
441
+ stopListening()
442
+ // HealthKit only has what the watch has synced. During a workout or a
443
+ // Mindfulness session on the Apple Watch that is a reading every few seconds;
444
+ // otherwise the watch measures every few minutes, and so do these. On a page
445
+ // the batches are `craftHealthHeartRate` events ({ samples }); native screens
446
+ // hear them too. In the simulator, the CraftFakeHealthHeartRate default
447
+ // (`xcrun simctl spawn booted defaults write <bundle id> CraftFakeHealthHeartRate -bool YES`)
448
+ // sends a made-up watch reading every 5 s instead: someone settling into slow
449
+ // breathing, from about 72 toward 60 bpm.
427
450
 
428
451
  // Screen Capture
429
452
  const screenshotBase64 = await window.craft.takeScreenshot();
@@ -619,7 +642,9 @@ const { samples } = await window.craft.heartRate.stopRecording(); // [{ timestam
619
642
  // answers { available: false }.
620
643
  // The simulator has no Bluetooth: `simctl spawn <device> defaults write
621
644
  // <bundle id> CraftFakeHeartRateMonitor -bool YES` stands in a strap ("Simulated
622
- // HRM") whose readings are encoded and decoded as 0x2A37 payloads. Simulator builds only.
645
+ // HRM") whose readings are encoded and decoded as 0x2A37 payloads, their RR
646
+ // intervals varying beat to beat with a ten-second breath as a heart's do, so a
647
+ // heart-rate variability worked out from them means something. Simulator builds only.
623
648
 
624
649
  // Apple Watch workouts (enableScheduledWorkouts, iOS 17+): WorkoutKit's
625
650
  // WorkoutScheduler puts a structured workout in the paired watch's Workout
package/dist/index.d.ts CHANGED
@@ -477,6 +477,7 @@ export declare function installRuntime(output: string, runtimeDir: string): Prom
477
477
  export declare function productName(name: string): string;
478
478
  /** Restore package-owned Swift into an existing project, without changing its Xcode metadata or bundled screens. */
479
479
  export declare function restoreShell(output: string): void;
480
+ export { assertNoApplicationSwiftSources, findApplicationSwiftSources } from './source-audit.js';
480
481
  export declare function init(options: InitOptions): Promise<void>;
481
482
  /**
482
483
  * Build web assets and generate Xcode project
package/dist/index.js CHANGED
@@ -1,15 +1,63 @@
1
1
  // @bun
2
2
  // src/index.ts
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";
3
+ import { cpSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
4
+ import { dirname as dirname2, join as join3, resolve } from "path";
5
5
  var {$ } = globalThis.Bun;
6
6
 
7
+ // src/source-audit.ts
8
+ import { readdirSync } from "fs";
9
+ import { join, relative, sep } from "path";
10
+ var defaultIgnoredPaths = [
11
+ ".git",
12
+ "node_modules",
13
+ "dist",
14
+ "build",
15
+ "storage/framework/mobile"
16
+ ];
17
+ function normalisePath(path) {
18
+ return path.split(sep).join("/");
19
+ }
20
+ function isIgnored(path, ignoredPaths) {
21
+ const normalised = normalisePath(path);
22
+ return ignoredPaths.some((ignored) => {
23
+ const prefix = normalisePath(ignored).replace(/\/+$/, "");
24
+ return normalised === prefix || normalised.startsWith(`${prefix}/`);
25
+ });
26
+ }
27
+ function visit(root, current, ignoredPaths, found) {
28
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
29
+ const absolute = join(current, entry.name);
30
+ const relativePath = relative(root, absolute);
31
+ if (isIgnored(relativePath, ignoredPaths))
32
+ continue;
33
+ if (entry.isDirectory()) {
34
+ visit(root, absolute, ignoredPaths, found);
35
+ continue;
36
+ }
37
+ if (entry.isFile() && entry.name.endsWith(".swift"))
38
+ found.push(normalisePath(relativePath));
39
+ }
40
+ }
41
+ function findApplicationSwiftSources(root, options = {}) {
42
+ const ignoredPaths = [...defaultIgnoredPaths, ...options.ignoredPaths ?? []];
43
+ const found = [];
44
+ visit(root, root, ignoredPaths, found);
45
+ return found.sort();
46
+ }
47
+ function assertNoApplicationSwiftSources(root, options = {}) {
48
+ const sources = findApplicationSwiftSources(root, options);
49
+ if (sources.length > 0) {
50
+ throw new Error(`Application-owned Swift sources found:
51
+ ${sources.map((path) => `- ${path}`).join(`
52
+ `)}`);
53
+ }
54
+ }
7
55
  // src/testing.ts
8
56
  import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
9
57
  import { tmpdir } from "os";
10
- import { dirname, join } from "path";
58
+ import { dirname, join as join2 } from "path";
11
59
  import { spawnSync } from "child_process";
12
- var templates = join(dirname(import.meta.dir), "templates/testing");
60
+ var templates = join2(dirname(import.meta.dir), "templates/testing");
13
61
  function bundleIdentifier(value) {
14
62
  if (!/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(value))
15
63
  throw new Error(`Invalid bundle identifier: ${value}`);
@@ -44,9 +92,9 @@ function initSimulatorDriver(options) {
44
92
  ["DriverTests.swift.template", "UITests/DriverTests.swift"],
45
93
  ["project.yml.template", "project.yml"]
46
94
  ]) {
47
- const path = join(output, destination);
95
+ const path = join2(output, destination);
48
96
  mkdirSync(dirname(path), { recursive: true });
49
- const content = readFileSync(join(templates, source), "utf8").replace(/\{\{([A-Z_]+)\}\}/g, (_, key) => replacements[key]);
97
+ const content = readFileSync(join2(templates, source), "utf8").replace(/\{\{([A-Z_]+)\}\}/g, (_, key) => replacements[key]);
50
98
  let previous;
51
99
  try {
52
100
  previous = readFileSync(path, "utf8");
@@ -108,11 +156,11 @@ function serializeSimulatorSteps(steps) {
108
156
  `;
109
157
  }
110
158
  function createNativeMutationValidator() {
111
- const output = mkdtempSync(join(tmpdir(), "craft-native-mutation-"));
112
- const binary = join(output, "validate");
159
+ const output = mkdtempSync(join2(tmpdir(), "craft-native-mutation-"));
160
+ const binary = join2(output, "validate");
113
161
  try {
114
- writeFileSync(join(output, "main.swift"), readFileSync(join(templates, "MutationValidator.swift")));
115
- const build = Bun.spawnSync(["swiftc", join(templates, "../CraftNativeMutation.swift"), join(output, "main.swift"), "-o", binary]);
162
+ writeFileSync(join2(output, "main.swift"), readFileSync(join2(templates, "MutationValidator.swift")));
163
+ const build = Bun.spawnSync(["swiftc", join2(templates, "../CraftNativeMutation.swift"), join2(output, "main.swift"), "-o", binary]);
116
164
  if (build.exitCode !== 0)
117
165
  throw new Error(build.stderr.toString());
118
166
  } catch (error) {
@@ -124,9 +172,9 @@ function createNativeMutationValidator() {
124
172
  validate(messages) {
125
173
  if (disposed)
126
174
  throw new Error("The native mutation validator has been disposed");
127
- const input = join(output, "messages.json");
175
+ const input = join2(output, "messages.json");
128
176
  writeFileSync(input, JSON.stringify(messages));
129
- const report = join(output, "result.txt");
177
+ const report = join2(output, "result.txt");
130
178
  rmSync(report, { force: true });
131
179
  const result = spawnSync(binary, [input, report], { encoding: "utf8" });
132
180
  let stdout = result.stdout ?? "";
@@ -143,12 +191,12 @@ function createNativeMutationValidator() {
143
191
  }
144
192
 
145
193
  // src/index.ts
146
- var TEMPLATES_DIR = join2(dirname2(import.meta.dir), "templates");
194
+ var TEMPLATES_DIR = join3(dirname2(import.meta.dir), "templates");
147
195
  function resolveCraftVersion(startDir = import.meta.dir) {
148
196
  let directory = resolve(startDir);
149
197
  while (true) {
150
198
  try {
151
- const packageJSON = JSON.parse(readFileSync2(join2(directory, "package.json"), "utf8"));
199
+ const packageJSON = JSON.parse(readFileSync2(join3(directory, "package.json"), "utf8"));
152
200
  if (packageJSON.name === "@craft-native/ios" && typeof packageJSON.version === "string" && packageJSON.version.length > 0)
153
201
  return packageJSON.version;
154
202
  } catch {}
@@ -300,7 +348,7 @@ function hybridConfigProblems(config) {
300
348
  function installNativeBundle(source, output) {
301
349
  if (!existsSync(source))
302
350
  throw new Error(`Native bundle not found: ${source}`);
303
- const destination = join2(output, "dist", "native-screen.js");
351
+ const destination = join3(output, "dist", "native-screen.js");
304
352
  mkdirSync2(dirname2(destination), { recursive: true });
305
353
  if (resolve(source) !== resolve(destination))
306
354
  cpSync(source, destination);
@@ -676,20 +724,20 @@ function writeOpaqueIcon(source, target) {
676
724
  }
677
725
  }
678
726
  function renderAssetCatalog(output, config) {
679
- const catalog = join2(output, "Assets.xcassets");
680
- const appIcon = join2(catalog, "AppIcon.appiconset");
681
- const launchBackground = join2(catalog, "LaunchBackground.colorset");
727
+ const catalog = join3(output, "Assets.xcassets");
728
+ const appIcon = join3(catalog, "AppIcon.appiconset");
729
+ const launchBackground = join3(catalog, "LaunchBackground.colorset");
682
730
  mkdirSync2(appIcon, { recursive: true });
683
731
  mkdirSync2(launchBackground, { recursive: true });
684
- writeFileSync2(join2(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
732
+ writeFileSync2(join3(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
685
733
  `);
686
734
  const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
687
735
  if (config.appIconPath) {
688
736
  if (!existsSync(config.appIconPath))
689
737
  throw new Error(`App icon not found: ${config.appIconPath}`);
690
- writeOpaqueIcon(config.appIconPath, join2(appIcon, iconFilename));
738
+ writeOpaqueIcon(config.appIconPath, join3(appIcon, iconFilename));
691
739
  }
692
- writeFileSync2(join2(appIcon, "Contents.json"), `${JSON.stringify({
740
+ writeFileSync2(join3(appIcon, "Contents.json"), `${JSON.stringify({
693
741
  images: iconFilename ? [{
694
742
  filename: iconFilename,
695
743
  idiom: "universal",
@@ -716,7 +764,7 @@ function renderAssetCatalog(output, config) {
716
764
  idiom: "universal"
717
765
  });
718
766
  }
719
- writeFileSync2(join2(launchBackground, "Contents.json"), `${JSON.stringify({
767
+ writeFileSync2(join3(launchBackground, "Contents.json"), `${JSON.stringify({
720
768
  colors,
721
769
  info: { author: "xcode", version: 1 }
722
770
  }, null, 2)}
@@ -725,7 +773,7 @@ function renderAssetCatalog(output, config) {
725
773
  }
726
774
  var LAUNCH_LOGO = "LaunchLogo";
727
775
  function renderLaunchLogo(catalog, config) {
728
- const imageset = join2(catalog, `${LAUNCH_LOGO}.imageset`);
776
+ const imageset = join3(catalog, `${LAUNCH_LOGO}.imageset`);
729
777
  rmSync2(imageset, { recursive: true, force: true });
730
778
  if (!config.splashImagePath)
731
779
  return;
@@ -741,14 +789,14 @@ function renderLaunchLogo(catalog, config) {
741
789
  if (extension === "svg" || extension === "pdf")
742
790
  vector = true;
743
791
  const filename = `${LAUNCH_LOGO}${dark ? "-dark" : ""}.${extension}`;
744
- cpSync(source, join2(imageset, filename));
792
+ cpSync(source, join3(imageset, filename));
745
793
  images.push({
746
794
  ...dark ? { appearances: [{ appearance: "luminosity", value: "dark" }] } : {},
747
795
  filename,
748
796
  idiom: "universal"
749
797
  });
750
798
  }
751
- writeFileSync2(join2(imageset, "Contents.json"), `${JSON.stringify({
799
+ writeFileSync2(join3(imageset, "Contents.json"), `${JSON.stringify({
752
800
  images,
753
801
  info: { author: "xcode", version: 1 },
754
802
  ...vector ? { properties: { "preserves-vector-representation": true } } : {}
@@ -862,15 +910,15 @@ function syncWebAssets(source, output) {
862
910
  const sourcePath = resolve(source);
863
911
  if (!existsSync(sourcePath))
864
912
  throw new Error(`Web asset path not found: ${source}`);
865
- const distDir = join2(output, "dist");
913
+ const distDir = join3(output, "dist");
866
914
  rmSync2(distDir, { recursive: true, force: true });
867
915
  mkdirSync2(distDir, { recursive: true });
868
916
  if (statSync(sourcePath).isDirectory()) {
869
917
  cpSync(sourcePath, distDir, { recursive: true });
870
918
  } else {
871
- cpSync(sourcePath, join2(distDir, "index.html"));
919
+ cpSync(sourcePath, join3(distDir, "index.html"));
872
920
  }
873
- if (!existsSync(join2(distDir, "index.html"))) {
921
+ if (!existsSync(join3(distDir, "index.html"))) {
874
922
  throw new Error(`Web asset directory must contain index.html: ${source}`);
875
923
  }
876
924
  }
@@ -892,32 +940,32 @@ var RUNTIME_ARCHIVES = {
892
940
  };
893
941
  async function installRuntime(output, runtimeDir) {
894
942
  const resolved = Object.entries(RUNTIME_ARCHIVES).map(([sdk, archives]) => {
895
- const present = archives.filter((a) => existsSync(join2(runtimeDir, a)));
943
+ const present = archives.filter((a) => existsSync(join3(runtimeDir, a)));
896
944
  if (present.length === 0) {
897
945
  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.`);
898
946
  }
899
947
  return { sdk, archives, present };
900
948
  });
901
- const dest = join2(output, "Runtime");
949
+ const dest = join3(output, "Runtime");
902
950
  rmSync2(dest, { recursive: true, force: true });
903
951
  for (const { sdk, archives, present } of resolved) {
904
- const sdkDir = join2(dest, sdk);
952
+ const sdkDir = join3(dest, sdk);
905
953
  mkdirSync2(sdkDir, { recursive: true });
906
- const target = join2(sdkDir, "libcraft-ios.a");
954
+ const target = join3(sdkDir, "libcraft-ios.a");
907
955
  if (present.length === 1) {
908
956
  const missing = archives.filter((a) => !present.includes(a));
909
957
  if (missing.length > 0) {
910
958
  console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
911
959
  }
912
- cpSync(join2(runtimeDir, present[0]), target);
960
+ cpSync(join3(runtimeDir, present[0]), target);
913
961
  } else {
914
- await $`lipo -create ${present.map((a) => join2(runtimeDir, a))} -output ${target}`.quiet();
962
+ await $`lipo -create ${present.map((a) => join3(runtimeDir, a))} -output ${target}`.quiet();
915
963
  }
916
964
  }
917
965
  return true;
918
966
  }
919
967
  async function refreshRuntime(output, override) {
920
- if (!existsSync(join2(output, "Runtime")))
968
+ if (!existsSync(join3(output, "Runtime")))
921
969
  return;
922
970
  const dir = resolveRuntimeDir(override);
923
971
  if (!dir) {
@@ -935,41 +983,41 @@ function productName(name) {
935
983
  return /^\d/.test(joined) ? `App${joined}` : joined;
936
984
  }
937
985
  function restoreShell(output) {
938
- const config = JSON.parse(readFileSync2(join2(output, "craft.config.json"), "utf8"));
986
+ const config = JSON.parse(readFileSync2(join3(output, "craft.config.json"), "utf8"));
939
987
  writeShell(output, config);
940
988
  }
941
989
  function writeShell(output, config) {
942
990
  if (!config.appName || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(config.bundleId))
943
991
  throw new Error("The iOS shell needs an appName and a valid bundleId in craft.config.json.");
944
992
  const name = productName(config.appName);
945
- mkdirSync2(join2(output, "Sources"), { recursive: true });
946
- mkdirSync2(join2(output, "Shared"), { recursive: true });
947
- const swiftTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
993
+ mkdirSync2(join3(output, "Sources"), { recursive: true });
994
+ mkdirSync2(join3(output, "Shared"), { recursive: true });
995
+ const swiftTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
948
996
  const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, config.bundleId).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION);
949
- writeFileSync2(join2(output, "Sources", `${name}App.swift`), swiftSource);
950
- cpSync(join2(TEMPLATES_DIR, "CraftNativeScreen.swift"), join2(output, "Sources", "CraftNativeScreen.swift"));
951
- cpSync(join2(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join2(output, "Sources", "CraftNativeFlatList.swift"));
952
- cpSync(join2(TEMPLATES_DIR, "CraftNativeActions.swift"), join2(output, "Sources", "CraftNativeActions.swift"));
953
- cpSync(join2(TEMPLATES_DIR, "CraftHealth.swift"), join2(output, "Sources", "CraftHealth.swift"));
954
- cpSync(join2(TEMPLATES_DIR, "CraftMusic.swift"), join2(output, "Sources", "CraftMusic.swift"));
955
- cpSync(join2(TEMPLATES_DIR, "CraftHeartRate.swift"), join2(output, "Sources", "CraftHeartRate.swift"));
956
- cpSync(join2(TEMPLATES_DIR, "CraftScheduledWorkouts.swift"), join2(output, "Sources", "CraftScheduledWorkouts.swift"));
957
- cpSync(join2(TEMPLATES_DIR, "CraftNativeMutation.swift"), join2(output, "Sources", "CraftNativeMutation.swift"));
958
- cpSync(join2(TEMPLATES_DIR, "CraftNativeUI.swift"), join2(output, "Sources", "CraftNativeUI.swift"));
959
- cpSync(join2(TEMPLATES_DIR, "CraftNativeServices.swift"), join2(output, "Sources", "CraftNativeServices.swift"));
960
- cpSync(join2(TEMPLATES_DIR, "CraftNativeFiles.swift"), join2(output, "Sources", "CraftNativeFiles.swift"));
961
- cpSync(join2(TEMPLATES_DIR, "CraftWebAudio.swift"), join2(output, "Sources", "CraftWebAudio.swift"));
962
- cpSync(join2(TEMPLATES_DIR, "CraftHybrid.swift"), join2(output, "Sources", "CraftHybrid.swift"));
963
- cpSync(join2(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join2(output, "Shared", "CraftActivityAttributes.swift"));
997
+ writeFileSync2(join3(output, "Sources", `${name}App.swift`), swiftSource);
998
+ cpSync(join3(TEMPLATES_DIR, "CraftNativeScreen.swift"), join3(output, "Sources", "CraftNativeScreen.swift"));
999
+ cpSync(join3(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join3(output, "Sources", "CraftNativeFlatList.swift"));
1000
+ cpSync(join3(TEMPLATES_DIR, "CraftNativeActions.swift"), join3(output, "Sources", "CraftNativeActions.swift"));
1001
+ cpSync(join3(TEMPLATES_DIR, "CraftHealth.swift"), join3(output, "Sources", "CraftHealth.swift"));
1002
+ cpSync(join3(TEMPLATES_DIR, "CraftMusic.swift"), join3(output, "Sources", "CraftMusic.swift"));
1003
+ cpSync(join3(TEMPLATES_DIR, "CraftHeartRate.swift"), join3(output, "Sources", "CraftHeartRate.swift"));
1004
+ cpSync(join3(TEMPLATES_DIR, "CraftScheduledWorkouts.swift"), join3(output, "Sources", "CraftScheduledWorkouts.swift"));
1005
+ cpSync(join3(TEMPLATES_DIR, "CraftNativeMutation.swift"), join3(output, "Sources", "CraftNativeMutation.swift"));
1006
+ cpSync(join3(TEMPLATES_DIR, "CraftNativeUI.swift"), join3(output, "Sources", "CraftNativeUI.swift"));
1007
+ cpSync(join3(TEMPLATES_DIR, "CraftNativeServices.swift"), join3(output, "Sources", "CraftNativeServices.swift"));
1008
+ cpSync(join3(TEMPLATES_DIR, "CraftNativeFiles.swift"), join3(output, "Sources", "CraftNativeFiles.swift"));
1009
+ cpSync(join3(TEMPLATES_DIR, "CraftWebAudio.swift"), join3(output, "Sources", "CraftWebAudio.swift"));
1010
+ cpSync(join3(TEMPLATES_DIR, "CraftHybrid.swift"), join3(output, "Sources", "CraftHybrid.swift"));
1011
+ cpSync(join3(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join3(output, "Shared", "CraftActivityAttributes.swift"));
964
1012
  for (const [enabled, folder, template, filename] of [
965
1013
  [config.enableLiveActivities, "WidgetExtension", "CraftLiveActivityWidget.swift.template", `${name}LiveActivity.swift`],
966
1014
  [config.enableWatchApp, "WatchExtension", "CraftWatchApp.swift.template", `${name}WatchApp.swift`]
967
1015
  ]) {
968
1016
  if (!enabled)
969
1017
  continue;
970
- mkdirSync2(join2(output, folder), { recursive: true });
971
- const source = readFileSync2(join2(TEMPLATES_DIR, template), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
972
- writeFileSync2(join2(output, folder, filename), source);
1018
+ mkdirSync2(join3(output, folder), { recursive: true });
1019
+ const source = readFileSync2(join3(TEMPLATES_DIR, template), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
1020
+ writeFileSync2(join3(output, folder, filename), source);
973
1021
  }
974
1022
  }
975
1023
  async function init(options) {
@@ -979,9 +1027,9 @@ async function init(options) {
979
1027
  \u26A1 Initializing Craft iOS project: ${displayName}`);
980
1028
  console.log(` Output: ${output}
981
1029
  `);
982
- const dirs = [output, join2(output, "Sources"), join2(output, "Shared"), join2(output, "dist")];
1030
+ const dirs = [output, join3(output, "Sources"), join3(output, "Shared"), join3(output, "dist")];
983
1031
  if (options.config?.enableWatchApp)
984
- dirs.push(join2(output, "WatchApp"), join2(output, "WatchExtension"));
1032
+ dirs.push(join3(output, "WatchApp"), join3(output, "WatchExtension"));
985
1033
  for (const dir of dirs) {
986
1034
  if (!existsSync(dir)) {
987
1035
  mkdirSync2(dir, { recursive: true });
@@ -1008,15 +1056,15 @@ async function init(options) {
1008
1056
  `));
1009
1057
  if (Object.keys(config.shareStorage ?? {}).length)
1010
1058
  config.enableSecureStorage = true;
1011
- writeFileSync2(join2(output, "craft.config.json"), JSON.stringify(config, null, 2));
1059
+ writeFileSync2(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
1012
1060
  writeShell(output, config);
1013
- const infoPlistTemplate = readFileSync2(join2(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
1061
+ const infoPlistTemplate = readFileSync2(join3(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
1014
1062
  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>
1015
1063
  <true/>
1016
1064
  <key>NSSupportsLiveActivitiesFrequentUpdates</key>
1017
1065
  <true/>` : "");
1018
- writeFileSync2(join2(output, "Info.plist"), infoPlist);
1019
- const projectYmlTemplate = readFileSync2(join2(TEMPLATES_DIR, "project.yml.template"), "utf-8");
1066
+ writeFileSync2(join3(output, "Info.plist"), infoPlist);
1067
+ const projectYmlTemplate = readFileSync2(join3(TEMPLATES_DIR, "project.yml.template"), "utf-8");
1020
1068
  const nativeDependencies = [];
1021
1069
  if (config.enableLiveActivities)
1022
1070
  nativeDependencies.push(` - target: ${name}LiveActivity`);
@@ -1073,30 +1121,30 @@ async function init(options) {
1073
1121
  if (hasRuntime) {
1074
1122
  console.log(" Linked the Zig runtime from", runtimeDir);
1075
1123
  } else {
1076
- rmSync2(join2(output, "Runtime"), { recursive: true, force: true });
1124
+ rmSync2(join3(output, "Runtime"), { recursive: true, force: true });
1077
1125
  }
1078
1126
  const projectYml = projectYmlTemplate.replace(/\{\{CRAFT_RUNTIME_SETTINGS\}\}/g, hasRuntime ? renderRuntimeSettings() : "").replace(/\{\{FEATURE_SETTINGS\}\}/g, renderFeatureSettings(config)).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:
1079
1127
  ${nativeDependencies.join(`
1080
1128
  `)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
1081
1129
  `));
1082
- writeFileSync2(join2(output, "project.yml"), projectYml);
1083
- writeFileSync2(join2(output, "Craft.entitlements"), renderEntitlements(config));
1084
- writeFileSync2(join2(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
1130
+ writeFileSync2(join3(output, "project.yml"), projectYml);
1131
+ writeFileSync2(join3(output, "Craft.entitlements"), renderEntitlements(config));
1132
+ writeFileSync2(join3(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
1085
1133
  renderAssetCatalog(output, config);
1086
1134
  if (config.enableLiveActivities) {
1087
- mkdirSync2(join2(output, "WidgetExtension"), { recursive: true });
1088
- const widgetInfo = readFileSync2(join2(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
1089
- writeFileSync2(join2(output, "WidgetExtension", "Info.plist"), widgetInfo);
1135
+ mkdirSync2(join3(output, "WidgetExtension"), { recursive: true });
1136
+ const widgetInfo = readFileSync2(join3(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
1137
+ writeFileSync2(join3(output, "WidgetExtension", "Info.plist"), widgetInfo);
1090
1138
  }
1091
1139
  if (config.enableWatchApp) {
1092
- const watchInfo = readFileSync2(join2(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1093
- writeFileSync2(join2(output, "WatchApp", "Info.plist"), watchInfo);
1094
- writeFileSync2(join2(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
1095
- const watchExtensionInfo = readFileSync2(join2(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1096
- writeFileSync2(join2(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
1097
- writeFileSync2(join2(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
1140
+ const watchInfo = readFileSync2(join3(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1141
+ writeFileSync2(join3(output, "WatchApp", "Info.plist"), watchInfo);
1142
+ writeFileSync2(join3(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
1143
+ const watchExtensionInfo = readFileSync2(join3(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1144
+ writeFileSync2(join3(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
1145
+ writeFileSync2(join3(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
1098
1146
  }
1099
- writeFileSync2(join2(output, "dist", "index.html"), renderOfflinePage(config));
1147
+ writeFileSync2(join3(output, "dist", "index.html"), renderOfflinePage(config));
1100
1148
  if (usesNativeBundle(config)) {
1101
1149
  if (config.nativeBundle)
1102
1150
  installNativeBundle(config.nativeBundle, output);
@@ -1123,7 +1171,7 @@ async function build(options) {
1123
1171
  const { htmlPath, nativeBundlePath, devServer, output, generateProject = true } = options;
1124
1172
  console.log(`
1125
1173
  \uD83D\uDCE6 Building Craft iOS project...`);
1126
- const configPath = join2(output, "craft.config.json");
1174
+ const configPath = join3(output, "craft.config.json");
1127
1175
  if (!existsSync(configPath)) {
1128
1176
  throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
1129
1177
  }
@@ -1138,7 +1186,7 @@ async function build(options) {
1138
1186
  const bundle = nativeBundlePath ?? config.nativeBundle;
1139
1187
  if (bundle)
1140
1188
  installNativeBundle(bundle, output);
1141
- if (!existsSync(join2(output, "dist", "native-screen.js"))) {
1189
+ if (!existsSync(join3(output, "dist", "native-screen.js"))) {
1142
1190
  throw new Error("Native iOS mode needs dist/native-screen.js. Compile a .stx screen with `stx native compile` first.");
1143
1191
  }
1144
1192
  } else if (nativeBundlePath && !isHybrid(config)) {
@@ -1164,7 +1212,7 @@ async function build(options) {
1164
1212
  if (bundle) {
1165
1213
  installNativeBundle(bundle, output);
1166
1214
  console.log(` Native screens: ${bundle} \u2192 dist/native-screen.js`);
1167
- } else if (!existsSync(join2(output, "dist", "native-screen.js"))) {
1215
+ } else if (!existsSync(join3(output, "dist", "native-screen.js"))) {
1168
1216
  console.warn(" \u26A0 nativeScreens is set but there is no native bundle: every path opens in the web view.");
1169
1217
  }
1170
1218
  }
@@ -1189,12 +1237,12 @@ async function build(options) {
1189
1237
  }
1190
1238
  async function open(options) {
1191
1239
  const { output } = options;
1192
- const files = readdirSync(output);
1240
+ const files = readdirSync2(output);
1193
1241
  const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
1194
1242
  if (!xcodeproj) {
1195
1243
  throw new Error(`No Xcode project found in ${output}. Run 'craft ios build' first.`);
1196
1244
  }
1197
- const projectPath = join2(output, xcodeproj);
1245
+ const projectPath = join3(output, xcodeproj);
1198
1246
  console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
1199
1247
  await $`open ${projectPath}`;
1200
1248
  }
@@ -1235,7 +1283,7 @@ async function showSimulator() {
1235
1283
  const selected = await $`xcode-select -p`.quiet().nothrow();
1236
1284
  const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
1237
1285
  if (developer) {
1238
- const app = join2(developer, "Applications", "Simulator.app");
1286
+ const app = join3(developer, "Applications", "Simulator.app");
1239
1287
  if (existsSync(app)) {
1240
1288
  await $`open ${app}`.quiet().nothrow();
1241
1289
  return;
@@ -1246,14 +1294,14 @@ async function showSimulator() {
1246
1294
  async function run(options) {
1247
1295
  const { simulator, output } = options;
1248
1296
  await build({ output });
1249
- const files = readdirSync(output);
1297
+ const files = readdirSync2(output);
1250
1298
  const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
1251
1299
  if (!xcodeproj) {
1252
1300
  throw new Error(`No Xcode project found in ${output}`);
1253
1301
  }
1254
- const projectPath = join2(output, xcodeproj);
1302
+ const projectPath = join3(output, xcodeproj);
1255
1303
  const appName = xcodeproj.replace(".xcodeproj", "");
1256
- const configPath = join2(output, "craft.config.json");
1304
+ const configPath = join3(output, "craft.config.json");
1257
1305
  if (!existsSync(configPath))
1258
1306
  throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
1259
1307
  const config = JSON.parse(readFileSync2(configPath, "utf-8"));
@@ -1266,8 +1314,8 @@ async function run(options) {
1266
1314
  console.log(` Device: ${device.name} (${device.runtime})`);
1267
1315
  try {
1268
1316
  await bootSimulator(device);
1269
- const derivedData = join2(output, "build");
1270
- const product = join2(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
1317
+ const derivedData = join3(output, "build");
1318
+ const product = join3(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
1271
1319
  await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
1272
1320
  if (!existsSync(product)) {
1273
1321
  throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
@@ -1330,10 +1378,12 @@ export {
1330
1378
  init,
1331
1379
  hybridConfigProblems,
1332
1380
  hasNativeRoutes,
1381
+ findApplicationSwiftSources,
1333
1382
  createNativeMutationValidator,
1334
1383
  build,
1335
1384
  bootSimulator,
1336
1385
  backgroundRefreshIdentifier,
1337
1386
  associatedDomainEntries,
1387
+ assertNoApplicationSwiftSources,
1338
1388
  LAUNCH_LOGO
1339
1389
  };
@@ -0,0 +1,8 @@
1
+ export interface SwiftSourceAuditOptions {
2
+ /** Paths, relative to `root`, that are generated or otherwise owned by Craft. */
3
+ ignoredPaths?: readonly string[];
4
+ }
5
+ /** Return Swift files owned by an application rather than Craft's generated shell. */
6
+ export declare function findApplicationSwiftSources(root: string, options?: SwiftSourceAuditOptions): string[];
7
+ /** Fail a build when an application has started maintaining its own Swift source. */
8
+ export declare function assertNoApplicationSwiftSources(root: string, options?: SwiftSourceAuditOptions): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-native/ios",
3
- "version": "0.0.164",
3
+ "version": "0.0.167",
4
4
  "homepage": "https://github.com/craft-native/craft/tree/main/packages/ios#readme",
5
5
  "repository": {
6
6
  "type": "git",