craft-native 0.0.164 → 0.0.166

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.
@@ -1105,6 +1105,8 @@ export interface NativeTabBarOptions {
1105
1105
  /** The current tab's colour, as `#rrggbb`; the system's label colour if unset. */
1106
1106
  tint?: string;
1107
1107
  tintDark?: string;
1108
+ /** Whether Craft's horizontal tab-switch gesture may begin over the page. Defaults to true. */
1109
+ swipeEnabled?: boolean;
1108
1110
  }
1109
1111
  /**
1110
1112
  * The phone's own tab bar, floating over the page: Liquid Glass on iOS 26 and
@@ -1535,8 +1537,12 @@ export interface HealthMindfulSample {
1535
1537
  externalId?: string;
1536
1538
  deviceName?: string;
1537
1539
  }
1540
+ export interface HealthHeartRateUpdatesOptions {
1541
+ /** Epoch milliseconds: samples that start at or after this. Defaults to two minutes ago. */
1542
+ since?: number;
1543
+ }
1538
1544
  /** The health reads and writes a host may or may not have, depending on the app build it runs in. */
1539
- export type HealthReadMethod = 'getWorkouts' | 'getDailyStatistics' | 'getHeartRateSamples' | 'getSleepIntervals' | 'saveData' | 'saveMindfulSession' | 'getMindfulSessions';
1545
+ export type HealthReadMethod = 'getWorkouts' | 'getDailyStatistics' | 'getHeartRateSamples' | 'getSleepIntervals' | 'saveData' | 'saveMindfulSession' | 'getMindfulSessions' | 'startHeartRateUpdates' | 'stopHeartRateUpdates';
1540
1546
  export declare const health: {
1541
1547
  /**
1542
1548
  * Whether this app build answers `method`. An app installed before a read
@@ -1569,6 +1575,24 @@ export declare const health: {
1569
1575
  saveMindfulSession(session: HealthMindfulSession): Promise<HealthMindfulSessionResult>;
1570
1576
  /** Mindful sessions between two instants, oldest first, each marked `ownApp` when this app wrote it. iOS only for now. */
1571
1577
  getMindfulSessions(options?: HealthMindfulSessionQuery): Promise<HealthMindfulSample[]>;
1578
+ /**
1579
+ * Heart rate as Apple Health receives it, until `stopHeartRateUpdates()`:
1580
+ * the samples since `since` (two minutes ago by default) first, then each
1581
+ * batch as the watch syncs it, to every `onHeartRate` listener. Needs the
1582
+ * `heartRate` grant. One stream at a time; starting again replaces it.
1583
+ * HealthKit only has what the watch has synced: every few seconds during a
1584
+ * workout or Mindfulness session on the watch, every few minutes otherwise.
1585
+ * iOS only for now.
1586
+ */
1587
+ startHeartRateUpdates(options?: HealthHeartRateUpdatesOptions): Promise<boolean>;
1588
+ /** Stops the stream `startHeartRateUpdates` started; true also when none ran. */
1589
+ stopHeartRateUpdates(): Promise<boolean>;
1590
+ /**
1591
+ * Each batch of new heart-rate samples while `startHeartRateUpdates` runs,
1592
+ * oldest first, every sample once. Returns the unsubscribe; off a phone, or
1593
+ * in a build without the stream, it never calls back.
1594
+ */
1595
+ onHeartRate(callback: (samples: HealthHeartRateSample[]) => void): () => void;
1572
1596
  };
1573
1597
  export interface LiveActivityState {
1574
1598
  status?: string;
package/dist/cli.js CHANGED
@@ -3303,7 +3303,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3303
3303
  `);
3304
3304
  }
3305
3305
  // package.json
3306
- var version = "0.0.164";
3306
+ var version = "0.0.166";
3307
3307
 
3308
3308
  // bin/cli.ts
3309
3309
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
@@ -40,9 +40,9 @@ export interface FlexStyle {
40
40
  flexWrap?: 'nowrap' | 'wrap' | 'wrap-reverse';
41
41
  /** Justify content (main axis) */
42
42
  justifyContent?: 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly';
43
- /** Align items (cross axis) */
43
+ /** Align items (cross axis); baseline aligns text-bearing children by their first baseline */
44
44
  alignItems?: 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
45
- /** Align self (override parent alignItems) */
45
+ /** Align self (override parent alignItems), including first-baseline alignment */
46
46
  alignSelf?: 'auto' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
47
47
  /** Align content (multi-line) */
48
48
  alignContent?: 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'space-between' | 'space-around' | 'space-evenly';
package/dist/index.cjs CHANGED
@@ -310,7 +310,7 @@ function craftBinaryNotFoundMessage(triedPath) {
310
310
  `);
311
311
  }
312
312
  // package.json
313
- var version = "0.0.164";
313
+ var version = "0.0.166";
314
314
 
315
315
  // src/sdk-version.ts
316
316
  var SDK_VERSION = version;
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ function craftBinaryNotFoundMessage(triedPath) {
58
58
  `);
59
59
  }
60
60
  // package.json
61
- var version = "0.0.164";
61
+ var version = "0.0.166";
62
62
 
63
63
  // src/sdk-version.ts
64
64
  var SDK_VERSION = version;
@@ -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
@@ -1,15 +1,63 @@
1
1
  // @bun
2
2
  // ../ios/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
+ // ../ios/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
  // ../ios/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
  // ../ios/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;