@craft-native/ios 0.0.172 → 0.0.174

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
@@ -120,7 +120,7 @@ ordinary native children while preserving the shared flex layout contract.
120
120
  Keyboard frame changes update the native controller's additional safe area, so
121
121
  focused inputs and responsive `onLayout` measurements move with the keyboard.
122
122
 
123
- The opt-in `native` renderer hosts compiled `.stx` screens in JavaScriptCore and renders their `View`, `SafeAreaView`, `Text`, `Button`, `Link`, `Checkbox`, `TextInput`, `Picker`, `DatePicker`, `Switch`, `Slider`, `ActivityIndicator`, `Icon`, `Image`, `Video`, `ScrollView`, `SectionList`, and recycling `FlatList` nodes as UIKit controls. `Icon` uses SF Symbols with the shared symbol/name contract and accessibility metadata. `SectionList` uses the same native scroll host for ordinary `.stx` slot children; grouped section data should use `FlatList` until the section-data protocol is available. `FlatList` uses `UICollectionView` with stable keyed rows, incremental diffs, grids, horizontal and inverted layouts, list chrome, and end-reached pagination. Native views support `onLayout` callbacks with point-based frame measurements that refresh after safe-area or orientation changes. Flex items support `flex`, `flexGrow`, `flexShrink`, and numeric `flexBasis` for main-axis space distribution. Native screens also expose the versioned capability bridge: JSON storage and SQLite survive relaunch, lifecycle and deep-link events are scoped to the top route, and local notifications use `UNUserNotificationCenter`. Enable each capability in `craft.config.json`; disabled calls preserve Craft's `CAPABILITY_DISABLED` rejection code and every request has deterministic timeout/cancellation behavior. The default remains `web`; existing iOS apps are unchanged. `Checkbox` supports controlled `checked`/`value`, indeterminate state, and boolean `onValueChange`; `Picker` renders `<select>`/`<option>` values with `onValueChange` or `onChange`; `DatePicker` emits local date/time values plus ISO and millisecond timestamps; `Switch` supports state-aware `trackColor`, `thumbColor`, and iOS `ios_backgroundColor`; `Slider` supports track/thumb tint props and `step` snapping; `Image` reports load lifecycle events and structured failures; `Video` uses an `AVPlayerViewController` attached to the native screen for URI playback, native controls, and route-safe teardown. The portable component, capability, list, style, image-source, event, accessibility, and reconciliation behavior is specified in the [Native STX guide](../../docs/guides/native-stx.md).
123
+ The opt-in `native` renderer hosts compiled `.stx` screens in JavaScriptCore and renders their `View`, `SafeAreaView`, `Text`, `Button`, `Link`, `Checkbox`, `TextInput`, `Picker`, `DatePicker`, `Switch`, `Slider`, `ActivityIndicator`, `Icon`, `Image`, `Video`, `Map`, `ScrollView`, `SectionList`, and recycling `FlatList` nodes as UIKit controls. `Map` uses MapKit for trail polylines and marker taps; its overlays carry flat latitude/longitude `geometry` arrays and can fit the visible region to their bounds. `Icon` uses SF Symbols with the shared symbol/name contract and accessibility metadata. `SectionList` uses the same native scroll host for ordinary `.stx` slot children; grouped section data should use `FlatList` until the section-data protocol is available. `FlatList` uses `UICollectionView` with stable keyed rows, incremental diffs, grids, horizontal and inverted layouts, list chrome, and end-reached pagination. Native views support `onLayout` callbacks with point-based frame measurements that refresh after safe-area or orientation changes. Flex items support `flex`, `flexGrow`, `flexShrink`, and numeric `flexBasis` for main-axis space distribution. Native screens also expose the versioned capability bridge: JSON storage and SQLite survive relaunch, lifecycle and deep-link events are scoped to the top route, and local notifications use `UNUserNotificationCenter`. Enable each capability in `craft.config.json`; disabled calls preserve Craft's `CAPABILITY_DISABLED` rejection code and every request has deterministic timeout/cancellation behavior. The default remains `web`; existing iOS apps are unchanged. `Checkbox` supports controlled `checked`/`value`, indeterminate state, and boolean `onValueChange`; `Picker` renders `<select>`/`<option>` values with `onValueChange` or `onChange`; `DatePicker` emits local date/time values plus ISO and millisecond timestamps; `Switch` supports state-aware `trackColor`, `thumbColor`, and iOS `ios_backgroundColor`; `Slider` supports track/thumb tint props and `step` snapping; `Image` reports load lifecycle events and structured failures; `Video` uses an `AVPlayerViewController` attached to the native screen for URI playback, native controls, and route-safe teardown. The portable component, capability, list, style, image-source, event, accessibility, and reconciliation behavior is specified in the [Native STX guide](../../docs/guides/native-stx.md).
124
124
 
125
125
  ```bash
126
126
  craft ios init NativeDemo --renderer native --output ./ios-native
@@ -131,6 +131,17 @@ craft ios run --output ./ios-native --simulator
131
131
 
132
132
  `stx native compile` is part of the [stx](https://github.com/stacksjs/stx) CLI (`@stacksjs/stx`); build tools can call `compileNativeBundle` from `@stacksjs/stx/native` instead. A runnable screen is in [`fixtures/native-screen/Screen.stx`](./fixtures/native-screen/Screen.stx). In native mode, the app never instantiates `WKWebView`; a missing `dist/native-screen.js` fails the build rather than silently opening the web placeholder.
133
133
 
134
+ Keep application repositories free of handwritten Swift with the built-in audit:
135
+
136
+ ```bash
137
+ craft-ios audit-sources --app-root .
138
+ ```
139
+
140
+ The command ignores Craft's generated `storage/framework/mobile` tree, build
141
+ output, and dependencies. It exits non-zero and lists the relative paths when
142
+ an app owns a `.swift` file; use the exported `assertNoApplicationSwiftSources`
143
+ function when the check belongs inside an existing TypeScript test.
144
+
134
145
  Native screen updates use the versioned mutation protocol when the bundle
135
146
  supports it and fall back to whole-document rendering for older bundles.
136
147
  Updates reconcile the UIKit tree by sibling-scoped `key` (or `testID` for
@@ -163,6 +174,23 @@ The optional Liquid Glass tab-bar styling is compiler-gated for Xcode 26/Swift 6
163
174
 
164
175
  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.
165
176
 
177
+ Native passkeys require both `enablePasskeys: true` and a matching
178
+ `webcredentials:<host>` entry in `associatedDomains`:
179
+
180
+ ```json
181
+ {
182
+ "renderer": "native",
183
+ "enablePasskeys": true,
184
+ "associatedDomains": ["webcredentials:login.example.com"]
185
+ }
186
+ ```
187
+
188
+ The native STX screen calls `craft.passkeys.isAvailable()`, `create(options)`,
189
+ or `get(options)`. The host validates the relying-party id against those
190
+ associated domains before showing Apple's `ASAuthorization` sheet; a passkey
191
+ request for another host is rejected. The server remains responsible for
192
+ generating the challenge and verifying the returned WebAuthn response.
193
+
166
194
  Edit `craft.config.json` in your iOS project:
167
195
 
168
196
  ```json
@@ -189,6 +217,8 @@ Edit `craft.config.json` in your iOS project:
189
217
  "enableFilePicker": true,
190
218
  "enableFileDownload": true,
191
219
  "enableSocialAuth": true,
220
+ "enablePasskeys": true,
221
+ "associatedDomains": ["webcredentials:login.example.com"],
192
222
  "enableAudioRecording": true,
193
223
  "enableVideoRecording": true,
194
224
  "enableMotionSensors": true,
package/dist/cli.js CHANGED
@@ -3,23 +3,72 @@
3
3
 
4
4
  // src/cli.ts
5
5
  import { parseArgs } from "util";
6
+ import { resolve as resolve2 } from "path";
6
7
 
7
8
  // src/index.ts
8
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
9
- import { dirname as dirname2, join as join2, resolve } from "path";
9
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync as readdirSync2, rmSync, statSync, writeFileSync } from "fs";
10
+ import { dirname as dirname2, join as join3, resolve } from "path";
10
11
  var {$ } = globalThis.Bun;
11
12
 
13
+ // src/source-audit.ts
14
+ import { readdirSync } from "fs";
15
+ import { join, relative, sep } from "path";
16
+ var defaultIgnoredPaths = [
17
+ ".git",
18
+ "node_modules",
19
+ "dist",
20
+ "build",
21
+ "storage/framework/mobile"
22
+ ];
23
+ function normalisePath(path) {
24
+ return path.split(sep).join("/");
25
+ }
26
+ function isIgnored(path, ignoredPaths) {
27
+ const normalised = normalisePath(path);
28
+ return ignoredPaths.some((ignored) => {
29
+ const prefix = normalisePath(ignored).replace(/\/+$/, "");
30
+ return normalised === prefix || normalised.startsWith(`${prefix}/`);
31
+ });
32
+ }
33
+ function visit(root, current, ignoredPaths, found) {
34
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
35
+ const absolute = join(current, entry.name);
36
+ const relativePath = relative(root, absolute);
37
+ if (isIgnored(relativePath, ignoredPaths))
38
+ continue;
39
+ if (entry.isDirectory()) {
40
+ visit(root, absolute, ignoredPaths, found);
41
+ continue;
42
+ }
43
+ if (entry.isFile() && entry.name.endsWith(".swift"))
44
+ found.push(normalisePath(relativePath));
45
+ }
46
+ }
47
+ function findApplicationSwiftSources(root, options = {}) {
48
+ const ignoredPaths = [...defaultIgnoredPaths, ...options.ignoredPaths ?? []];
49
+ const found = [];
50
+ visit(root, root, ignoredPaths, found);
51
+ return found.sort();
52
+ }
53
+ function assertNoApplicationSwiftSources(root, options = {}) {
54
+ const sources = findApplicationSwiftSources(root, options);
55
+ if (sources.length > 0) {
56
+ throw new Error(`Application-owned Swift sources found:
57
+ ${sources.map((path) => `- ${path}`).join(`
58
+ `)}`);
59
+ }
60
+ }
12
61
  // src/testing.ts
13
- import { dirname, join } from "path";
14
- var templates = join(dirname(import.meta.dir), "templates/testing");
62
+ import { dirname, join as join2 } from "path";
63
+ var templates = join2(dirname(import.meta.dir), "templates/testing");
15
64
 
16
65
  // src/index.ts
17
- var TEMPLATES_DIR = join2(dirname2(import.meta.dir), "templates");
66
+ var TEMPLATES_DIR = join3(dirname2(import.meta.dir), "templates");
18
67
  function resolveCraftVersion(startDir = import.meta.dir) {
19
68
  let directory = resolve(startDir);
20
69
  while (true) {
21
70
  try {
22
- const packageJSON = JSON.parse(readFileSync(join2(directory, "package.json"), "utf8"));
71
+ const packageJSON = JSON.parse(readFileSync(join3(directory, "package.json"), "utf8"));
23
72
  if (packageJSON.name === "@craft-native/ios" && typeof packageJSON.version === "string" && packageJSON.version.length > 0)
24
73
  return packageJSON.version;
25
74
  } catch {}
@@ -171,7 +220,7 @@ function hybridConfigProblems(config) {
171
220
  function installNativeBundle(source, output) {
172
221
  if (!existsSync(source))
173
222
  throw new Error(`Native bundle not found: ${source}`);
174
- const destination = join2(output, "dist", "native-screen.js");
223
+ const destination = join3(output, "dist", "native-screen.js");
175
224
  mkdirSync(dirname2(destination), { recursive: true });
176
225
  if (resolve(source) !== resolve(destination))
177
226
  cpSync(source, destination);
@@ -203,6 +252,7 @@ var DEFAULT_CONFIG = {
203
252
  enableFilePicker: false,
204
253
  enableFileDownload: false,
205
254
  enableSocialAuth: false,
255
+ enablePasskeys: false,
206
256
  enableAudioRecording: false,
207
257
  enableVideoRecording: false,
208
258
  enableMotionSensors: false,
@@ -547,20 +597,20 @@ function writeOpaqueIcon(source, target) {
547
597
  }
548
598
  }
549
599
  function renderAssetCatalog(output, config) {
550
- const catalog = join2(output, "Assets.xcassets");
551
- const appIcon = join2(catalog, "AppIcon.appiconset");
552
- const launchBackground = join2(catalog, "LaunchBackground.colorset");
600
+ const catalog = join3(output, "Assets.xcassets");
601
+ const appIcon = join3(catalog, "AppIcon.appiconset");
602
+ const launchBackground = join3(catalog, "LaunchBackground.colorset");
553
603
  mkdirSync(appIcon, { recursive: true });
554
604
  mkdirSync(launchBackground, { recursive: true });
555
- writeFileSync(join2(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
605
+ writeFileSync(join3(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
556
606
  `);
557
607
  const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
558
608
  if (config.appIconPath) {
559
609
  if (!existsSync(config.appIconPath))
560
610
  throw new Error(`App icon not found: ${config.appIconPath}`);
561
- writeOpaqueIcon(config.appIconPath, join2(appIcon, iconFilename));
611
+ writeOpaqueIcon(config.appIconPath, join3(appIcon, iconFilename));
562
612
  }
563
- writeFileSync(join2(appIcon, "Contents.json"), `${JSON.stringify({
613
+ writeFileSync(join3(appIcon, "Contents.json"), `${JSON.stringify({
564
614
  images: iconFilename ? [{
565
615
  filename: iconFilename,
566
616
  idiom: "universal",
@@ -587,7 +637,7 @@ function renderAssetCatalog(output, config) {
587
637
  idiom: "universal"
588
638
  });
589
639
  }
590
- writeFileSync(join2(launchBackground, "Contents.json"), `${JSON.stringify({
640
+ writeFileSync(join3(launchBackground, "Contents.json"), `${JSON.stringify({
591
641
  colors,
592
642
  info: { author: "xcode", version: 1 }
593
643
  }, null, 2)}
@@ -596,7 +646,7 @@ function renderAssetCatalog(output, config) {
596
646
  }
597
647
  var LAUNCH_LOGO = "LaunchLogo";
598
648
  function renderLaunchLogo(catalog, config) {
599
- const imageset = join2(catalog, `${LAUNCH_LOGO}.imageset`);
649
+ const imageset = join3(catalog, `${LAUNCH_LOGO}.imageset`);
600
650
  rmSync(imageset, { recursive: true, force: true });
601
651
  if (!config.splashImagePath)
602
652
  return;
@@ -612,14 +662,14 @@ function renderLaunchLogo(catalog, config) {
612
662
  if (extension === "svg" || extension === "pdf")
613
663
  vector = true;
614
664
  const filename = `${LAUNCH_LOGO}${dark ? "-dark" : ""}.${extension}`;
615
- cpSync(source, join2(imageset, filename));
665
+ cpSync(source, join3(imageset, filename));
616
666
  images.push({
617
667
  ...dark ? { appearances: [{ appearance: "luminosity", value: "dark" }] } : {},
618
668
  filename,
619
669
  idiom: "universal"
620
670
  });
621
671
  }
622
- writeFileSync(join2(imageset, "Contents.json"), `${JSON.stringify({
672
+ writeFileSync(join3(imageset, "Contents.json"), `${JSON.stringify({
623
673
  images,
624
674
  info: { author: "xcode", version: 1 },
625
675
  ...vector ? { properties: { "preserves-vector-representation": true } } : {}
@@ -733,15 +783,15 @@ function syncWebAssets(source, output) {
733
783
  const sourcePath = resolve(source);
734
784
  if (!existsSync(sourcePath))
735
785
  throw new Error(`Web asset path not found: ${source}`);
736
- const distDir = join2(output, "dist");
786
+ const distDir = join3(output, "dist");
737
787
  rmSync(distDir, { recursive: true, force: true });
738
788
  mkdirSync(distDir, { recursive: true });
739
789
  if (statSync(sourcePath).isDirectory()) {
740
790
  cpSync(sourcePath, distDir, { recursive: true });
741
791
  } else {
742
- cpSync(sourcePath, join2(distDir, "index.html"));
792
+ cpSync(sourcePath, join3(distDir, "index.html"));
743
793
  }
744
- if (!existsSync(join2(distDir, "index.html"))) {
794
+ if (!existsSync(join3(distDir, "index.html"))) {
745
795
  throw new Error(`Web asset directory must contain index.html: ${source}`);
746
796
  }
747
797
  }
@@ -763,32 +813,32 @@ var RUNTIME_ARCHIVES = {
763
813
  };
764
814
  async function installRuntime(output, runtimeDir) {
765
815
  const resolved = Object.entries(RUNTIME_ARCHIVES).map(([sdk, archives]) => {
766
- const present = archives.filter((a) => existsSync(join2(runtimeDir, a)));
816
+ const present = archives.filter((a) => existsSync(join3(runtimeDir, a)));
767
817
  if (present.length === 0) {
768
818
  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.`);
769
819
  }
770
820
  return { sdk, archives, present };
771
821
  });
772
- const dest = join2(output, "Runtime");
822
+ const dest = join3(output, "Runtime");
773
823
  rmSync(dest, { recursive: true, force: true });
774
824
  for (const { sdk, archives, present } of resolved) {
775
- const sdkDir = join2(dest, sdk);
825
+ const sdkDir = join3(dest, sdk);
776
826
  mkdirSync(sdkDir, { recursive: true });
777
- const target = join2(sdkDir, "libcraft-ios.a");
827
+ const target = join3(sdkDir, "libcraft-ios.a");
778
828
  if (present.length === 1) {
779
829
  const missing = archives.filter((a) => !present.includes(a));
780
830
  if (missing.length > 0) {
781
831
  console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
782
832
  }
783
- cpSync(join2(runtimeDir, present[0]), target);
833
+ cpSync(join3(runtimeDir, present[0]), target);
784
834
  } else {
785
- await $`lipo -create ${present.map((a) => join2(runtimeDir, a))} -output ${target}`.quiet();
835
+ await $`lipo -create ${present.map((a) => join3(runtimeDir, a))} -output ${target}`.quiet();
786
836
  }
787
837
  }
788
838
  return true;
789
839
  }
790
840
  async function refreshRuntime(output, override) {
791
- if (!existsSync(join2(output, "Runtime")))
841
+ if (!existsSync(join3(output, "Runtime")))
792
842
  return;
793
843
  const dir = resolveRuntimeDir(override);
794
844
  if (!dir) {
@@ -809,34 +859,34 @@ function writeShell(output, config) {
809
859
  if (!config.appName || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(config.bundleId))
810
860
  throw new Error("The iOS shell needs an appName and a valid bundleId in craft.config.json.");
811
861
  const name = productName(config.appName);
812
- mkdirSync(join2(output, "Sources"), { recursive: true });
813
- mkdirSync(join2(output, "Shared"), { recursive: true });
814
- const swiftTemplate = readFileSync(join2(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
862
+ mkdirSync(join3(output, "Sources"), { recursive: true });
863
+ mkdirSync(join3(output, "Shared"), { recursive: true });
864
+ const swiftTemplate = readFileSync(join3(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
815
865
  const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, config.bundleId).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION);
816
- writeFileSync(join2(output, "Sources", `${name}App.swift`), swiftSource);
817
- cpSync(join2(TEMPLATES_DIR, "CraftNativeScreen.swift"), join2(output, "Sources", "CraftNativeScreen.swift"));
818
- cpSync(join2(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join2(output, "Sources", "CraftNativeFlatList.swift"));
819
- cpSync(join2(TEMPLATES_DIR, "CraftNativeActions.swift"), join2(output, "Sources", "CraftNativeActions.swift"));
820
- cpSync(join2(TEMPLATES_DIR, "CraftHealth.swift"), join2(output, "Sources", "CraftHealth.swift"));
821
- cpSync(join2(TEMPLATES_DIR, "CraftMusic.swift"), join2(output, "Sources", "CraftMusic.swift"));
822
- cpSync(join2(TEMPLATES_DIR, "CraftHeartRate.swift"), join2(output, "Sources", "CraftHeartRate.swift"));
823
- cpSync(join2(TEMPLATES_DIR, "CraftScheduledWorkouts.swift"), join2(output, "Sources", "CraftScheduledWorkouts.swift"));
824
- cpSync(join2(TEMPLATES_DIR, "CraftNativeMutation.swift"), join2(output, "Sources", "CraftNativeMutation.swift"));
825
- cpSync(join2(TEMPLATES_DIR, "CraftNativeUI.swift"), join2(output, "Sources", "CraftNativeUI.swift"));
826
- cpSync(join2(TEMPLATES_DIR, "CraftNativeServices.swift"), join2(output, "Sources", "CraftNativeServices.swift"));
827
- cpSync(join2(TEMPLATES_DIR, "CraftNativeFiles.swift"), join2(output, "Sources", "CraftNativeFiles.swift"));
828
- cpSync(join2(TEMPLATES_DIR, "CraftWebAudio.swift"), join2(output, "Sources", "CraftWebAudio.swift"));
829
- cpSync(join2(TEMPLATES_DIR, "CraftHybrid.swift"), join2(output, "Sources", "CraftHybrid.swift"));
830
- cpSync(join2(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join2(output, "Shared", "CraftActivityAttributes.swift"));
866
+ writeFileSync(join3(output, "Sources", `${name}App.swift`), swiftSource);
867
+ cpSync(join3(TEMPLATES_DIR, "CraftNativeScreen.swift"), join3(output, "Sources", "CraftNativeScreen.swift"));
868
+ cpSync(join3(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join3(output, "Sources", "CraftNativeFlatList.swift"));
869
+ cpSync(join3(TEMPLATES_DIR, "CraftNativeActions.swift"), join3(output, "Sources", "CraftNativeActions.swift"));
870
+ cpSync(join3(TEMPLATES_DIR, "CraftHealth.swift"), join3(output, "Sources", "CraftHealth.swift"));
871
+ cpSync(join3(TEMPLATES_DIR, "CraftMusic.swift"), join3(output, "Sources", "CraftMusic.swift"));
872
+ cpSync(join3(TEMPLATES_DIR, "CraftHeartRate.swift"), join3(output, "Sources", "CraftHeartRate.swift"));
873
+ cpSync(join3(TEMPLATES_DIR, "CraftScheduledWorkouts.swift"), join3(output, "Sources", "CraftScheduledWorkouts.swift"));
874
+ cpSync(join3(TEMPLATES_DIR, "CraftNativeMutation.swift"), join3(output, "Sources", "CraftNativeMutation.swift"));
875
+ cpSync(join3(TEMPLATES_DIR, "CraftNativeUI.swift"), join3(output, "Sources", "CraftNativeUI.swift"));
876
+ cpSync(join3(TEMPLATES_DIR, "CraftNativeServices.swift"), join3(output, "Sources", "CraftNativeServices.swift"));
877
+ cpSync(join3(TEMPLATES_DIR, "CraftNativeFiles.swift"), join3(output, "Sources", "CraftNativeFiles.swift"));
878
+ cpSync(join3(TEMPLATES_DIR, "CraftWebAudio.swift"), join3(output, "Sources", "CraftWebAudio.swift"));
879
+ cpSync(join3(TEMPLATES_DIR, "CraftHybrid.swift"), join3(output, "Sources", "CraftHybrid.swift"));
880
+ cpSync(join3(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join3(output, "Shared", "CraftActivityAttributes.swift"));
831
881
  for (const [enabled, folder, template, filename] of [
832
882
  [config.enableLiveActivities, "WidgetExtension", "CraftLiveActivityWidget.swift.template", `${name}LiveActivity.swift`],
833
883
  [config.enableWatchApp, "WatchExtension", "CraftWatchApp.swift.template", `${name}WatchApp.swift`]
834
884
  ]) {
835
885
  if (!enabled)
836
886
  continue;
837
- mkdirSync(join2(output, folder), { recursive: true });
838
- const source = readFileSync(join2(TEMPLATES_DIR, template), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
839
- writeFileSync(join2(output, folder, filename), source);
887
+ mkdirSync(join3(output, folder), { recursive: true });
888
+ const source = readFileSync(join3(TEMPLATES_DIR, template), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
889
+ writeFileSync(join3(output, folder, filename), source);
840
890
  }
841
891
  }
842
892
  async function init(options) {
@@ -846,9 +896,9 @@ async function init(options) {
846
896
  \u26A1 Initializing Craft iOS project: ${displayName}`);
847
897
  console.log(` Output: ${output}
848
898
  `);
849
- const dirs = [output, join2(output, "Sources"), join2(output, "Shared"), join2(output, "dist")];
899
+ const dirs = [output, join3(output, "Sources"), join3(output, "Shared"), join3(output, "dist")];
850
900
  if (options.config?.enableWatchApp)
851
- dirs.push(join2(output, "WatchApp"), join2(output, "WatchExtension"));
901
+ dirs.push(join3(output, "WatchApp"), join3(output, "WatchExtension"));
852
902
  for (const dir of dirs) {
853
903
  if (!existsSync(dir)) {
854
904
  mkdirSync(dir, { recursive: true });
@@ -875,15 +925,15 @@ async function init(options) {
875
925
  `));
876
926
  if (Object.keys(config.shareStorage ?? {}).length)
877
927
  config.enableSecureStorage = true;
878
- writeFileSync(join2(output, "craft.config.json"), JSON.stringify(config, null, 2));
928
+ writeFileSync(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
879
929
  writeShell(output, config);
880
- const infoPlistTemplate = readFileSync(join2(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
930
+ const infoPlistTemplate = readFileSync(join3(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
881
931
  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>
882
932
  <true/>
883
933
  <key>NSSupportsLiveActivitiesFrequentUpdates</key>
884
934
  <true/>` : "");
885
- writeFileSync(join2(output, "Info.plist"), infoPlist);
886
- const projectYmlTemplate = readFileSync(join2(TEMPLATES_DIR, "project.yml.template"), "utf-8");
935
+ writeFileSync(join3(output, "Info.plist"), infoPlist);
936
+ const projectYmlTemplate = readFileSync(join3(TEMPLATES_DIR, "project.yml.template"), "utf-8");
887
937
  const nativeDependencies = [];
888
938
  if (config.enableLiveActivities)
889
939
  nativeDependencies.push(` - target: ${name}LiveActivity`);
@@ -940,30 +990,30 @@ async function init(options) {
940
990
  if (hasRuntime) {
941
991
  console.log(" Linked the Zig runtime from", runtimeDir);
942
992
  } else {
943
- rmSync(join2(output, "Runtime"), { recursive: true, force: true });
993
+ rmSync(join3(output, "Runtime"), { recursive: true, force: true });
944
994
  }
945
995
  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:
946
996
  ${nativeDependencies.join(`
947
997
  `)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
948
998
  `));
949
- writeFileSync(join2(output, "project.yml"), projectYml);
950
- writeFileSync(join2(output, "Craft.entitlements"), renderEntitlements(config));
951
- writeFileSync(join2(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
999
+ writeFileSync(join3(output, "project.yml"), projectYml);
1000
+ writeFileSync(join3(output, "Craft.entitlements"), renderEntitlements(config));
1001
+ writeFileSync(join3(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
952
1002
  renderAssetCatalog(output, config);
953
1003
  if (config.enableLiveActivities) {
954
- mkdirSync(join2(output, "WidgetExtension"), { recursive: true });
955
- const widgetInfo = readFileSync(join2(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
956
- writeFileSync(join2(output, "WidgetExtension", "Info.plist"), widgetInfo);
1004
+ mkdirSync(join3(output, "WidgetExtension"), { recursive: true });
1005
+ const widgetInfo = readFileSync(join3(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
1006
+ writeFileSync(join3(output, "WidgetExtension", "Info.plist"), widgetInfo);
957
1007
  }
958
1008
  if (config.enableWatchApp) {
959
- const watchInfo = readFileSync(join2(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
960
- writeFileSync(join2(output, "WatchApp", "Info.plist"), watchInfo);
961
- writeFileSync(join2(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
962
- const watchExtensionInfo = readFileSync(join2(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
963
- writeFileSync(join2(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
964
- writeFileSync(join2(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
1009
+ const watchInfo = readFileSync(join3(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1010
+ writeFileSync(join3(output, "WatchApp", "Info.plist"), watchInfo);
1011
+ writeFileSync(join3(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
1012
+ const watchExtensionInfo = readFileSync(join3(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
1013
+ writeFileSync(join3(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
1014
+ writeFileSync(join3(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
965
1015
  }
966
- writeFileSync(join2(output, "dist", "index.html"), renderOfflinePage(config));
1016
+ writeFileSync(join3(output, "dist", "index.html"), renderOfflinePage(config));
967
1017
  if (usesNativeBundle(config)) {
968
1018
  if (config.nativeBundle)
969
1019
  installNativeBundle(config.nativeBundle, output);
@@ -990,7 +1040,7 @@ async function build(options) {
990
1040
  const { htmlPath, nativeBundlePath, devServer, output, generateProject = true } = options;
991
1041
  console.log(`
992
1042
  \uD83D\uDCE6 Building Craft iOS project...`);
993
- const configPath = join2(output, "craft.config.json");
1043
+ const configPath = join3(output, "craft.config.json");
994
1044
  if (!existsSync(configPath)) {
995
1045
  throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
996
1046
  }
@@ -1005,7 +1055,7 @@ async function build(options) {
1005
1055
  const bundle = nativeBundlePath ?? config.nativeBundle;
1006
1056
  if (bundle)
1007
1057
  installNativeBundle(bundle, output);
1008
- if (!existsSync(join2(output, "dist", "native-screen.js"))) {
1058
+ if (!existsSync(join3(output, "dist", "native-screen.js"))) {
1009
1059
  throw new Error("Native iOS mode needs dist/native-screen.js. Compile a .stx screen with `stx native compile` first.");
1010
1060
  }
1011
1061
  } else if (nativeBundlePath && !isHybrid(config)) {
@@ -1031,7 +1081,7 @@ async function build(options) {
1031
1081
  if (bundle) {
1032
1082
  installNativeBundle(bundle, output);
1033
1083
  console.log(` Native screens: ${bundle} \u2192 dist/native-screen.js`);
1034
- } else if (!existsSync(join2(output, "dist", "native-screen.js"))) {
1084
+ } else if (!existsSync(join3(output, "dist", "native-screen.js"))) {
1035
1085
  console.warn(" \u26A0 nativeScreens is set but there is no native bundle: every path opens in the web view.");
1036
1086
  }
1037
1087
  }
@@ -1056,12 +1106,12 @@ async function build(options) {
1056
1106
  }
1057
1107
  async function open(options) {
1058
1108
  const { output } = options;
1059
- const files = readdirSync(output);
1109
+ const files = readdirSync2(output);
1060
1110
  const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
1061
1111
  if (!xcodeproj) {
1062
1112
  throw new Error(`No Xcode project found in ${output}. Run 'craft ios build' first.`);
1063
1113
  }
1064
- const projectPath = join2(output, xcodeproj);
1114
+ const projectPath = join3(output, xcodeproj);
1065
1115
  console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
1066
1116
  await $`open ${projectPath}`;
1067
1117
  }
@@ -1102,7 +1152,7 @@ async function showSimulator() {
1102
1152
  const selected = await $`xcode-select -p`.quiet().nothrow();
1103
1153
  const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
1104
1154
  if (developer) {
1105
- const app = join2(developer, "Applications", "Simulator.app");
1155
+ const app = join3(developer, "Applications", "Simulator.app");
1106
1156
  if (existsSync(app)) {
1107
1157
  await $`open ${app}`.quiet().nothrow();
1108
1158
  return;
@@ -1113,14 +1163,14 @@ async function showSimulator() {
1113
1163
  async function run(options) {
1114
1164
  const { simulator, output } = options;
1115
1165
  await build({ output });
1116
- const files = readdirSync(output);
1166
+ const files = readdirSync2(output);
1117
1167
  const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
1118
1168
  if (!xcodeproj) {
1119
1169
  throw new Error(`No Xcode project found in ${output}`);
1120
1170
  }
1121
- const projectPath = join2(output, xcodeproj);
1171
+ const projectPath = join3(output, xcodeproj);
1122
1172
  const appName = xcodeproj.replace(".xcodeproj", "");
1123
- const configPath = join2(output, "craft.config.json");
1173
+ const configPath = join3(output, "craft.config.json");
1124
1174
  if (!existsSync(configPath))
1125
1175
  throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
1126
1176
  const config = JSON.parse(readFileSync(configPath, "utf-8"));
@@ -1133,8 +1183,8 @@ async function run(options) {
1133
1183
  console.log(` Device: ${device.name} (${device.runtime})`);
1134
1184
  try {
1135
1185
  await bootSimulator(device);
1136
- const derivedData = join2(output, "build");
1137
- const product = join2(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
1186
+ const derivedData = join3(output, "build");
1187
+ const product = join3(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
1138
1188
  await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
1139
1189
  if (!existsSync(product)) {
1140
1190
  throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
@@ -1171,6 +1221,7 @@ var { values, positionals } = parseArgs({
1171
1221
  "html-path": { type: "string" },
1172
1222
  "native-bundle": { type: "string" },
1173
1223
  "dev-server": { type: "string", short: "d" },
1224
+ "app-root": { type: "string" },
1174
1225
  output: { type: "string", short: "o" },
1175
1226
  simulator: { type: "boolean", short: "s" }
1176
1227
  },
@@ -1187,6 +1238,7 @@ Usage:
1187
1238
  Commands:
1188
1239
  init <name> Initialize a new iOS project
1189
1240
  build Build web assets and generate Xcode project
1241
+ audit-sources Fail when the app owns Swift outside Craft's generated output
1190
1242
  open Open Xcode project
1191
1243
  run Build and run on simulator
1192
1244
 
@@ -1198,6 +1250,7 @@ Options:
1198
1250
  --html-path <path> Path to HTML file or directory
1199
1251
  --native-bundle <path> JavaScript bundle from \`stx native compile\`
1200
1252
  -d, --dev-server <url> Development server URL
1253
+ --app-root <dir> Application root for audit-sources (default: .)
1201
1254
  -o, --output <dir> Output directory (default: ./ios)
1202
1255
  -s, --simulator Run on simulator instead of device
1203
1256
  -h, --help Show help
@@ -1208,6 +1261,7 @@ Examples:
1208
1261
  craft-ios build --html-path ./dist/index.html
1209
1262
  craft-ios init MyApp --renderer native
1210
1263
  craft-ios build --native-bundle ./screen.js
1264
+ craft-ios audit-sources --app-root .
1211
1265
  craft-ios build --dev-server http://localhost:3456
1212
1266
  craft-ios run --simulator
1213
1267
  `);
@@ -1237,6 +1291,12 @@ async function main() {
1237
1291
  output: values.output || "./ios"
1238
1292
  });
1239
1293
  break;
1294
+ case "audit-sources": {
1295
+ const root = resolve2(values["app-root"] || ".");
1296
+ assertNoApplicationSwiftSources(root);
1297
+ console.log(`\u2705 No application-owned Swift sources found in ${root}`);
1298
+ break;
1299
+ }
1240
1300
  case "open":
1241
1301
  await open({
1242
1302
  output: values.output || "./ios"
package/dist/index.d.ts CHANGED
@@ -47,6 +47,8 @@ export interface CraftConfig {
47
47
  enableFilePicker?: boolean;
48
48
  enableFileDownload?: boolean;
49
49
  enableSocialAuth?: boolean;
50
+ /** Native passkeys; requires a matching `webcredentials:` associated domain. */
51
+ enablePasskeys?: boolean;
50
52
  enableAudioRecording?: boolean;
51
53
  enableVideoRecording?: boolean;
52
54
  enableMotionSensors?: boolean;
package/dist/index.js CHANGED
@@ -380,6 +380,7 @@ var DEFAULT_CONFIG = {
380
380
  enableFilePicker: false,
381
381
  enableFileDownload: false,
382
382
  enableSocialAuth: false,
383
+ enablePasskeys: false,
383
384
  enableAudioRecording: false,
384
385
  enableVideoRecording: false,
385
386
  enableMotionSensors: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-native/ios",
3
- "version": "0.0.172",
3
+ "version": "0.0.174",
4
4
  "homepage": "https://github.com/craft-native/craft/tree/main/packages/ios#readme",
5
5
  "repository": {
6
6
  "type": "git",
@@ -561,7 +561,13 @@ struct CraftApp: App {
561
561
  .ignoresSafeArea(.keyboard, edges: .bottom)
562
562
  } else if appState.config.renderer == "native" {
563
563
  // One native screen and its stack: no paths, no tabs.
564
+ // The screen keeps itself out of the keyboard's way (its
565
+ // own bottom safe area, CraftNativeScreen.swift); SwiftUI
566
+ // shrinking it as well took the keyboard's height twice,
567
+ // and a screen's scroll views and text views were squeezed
568
+ // to nothing whenever the keyboard came up.
564
569
  CraftNativeScreen(config: appState.config)
570
+ .ignoresSafeArea(.keyboard, edges: .bottom)
565
571
  } else {
566
572
  // The page's own chrome, drawn natively over it: the tab
567
573
  // bar the page asks for, and the splash until it is ready.
@@ -1233,6 +1239,9 @@ struct CraftConfig: Codable {
1233
1239
  var enableFilePicker: Bool = false
1234
1240
  var enableFileDownload: Bool = false
1235
1241
  var enableSocialAuth: Bool = false
1242
+ /// Native passkeys through AuthenticationServices. Requires a matching
1243
+ /// `webcredentials:` associated-domain entry.
1244
+ var enablePasskeys: Bool = false
1236
1245
  var enableAudioRecording: Bool = false
1237
1246
  var enableVideoRecording: Bool = false
1238
1247
  var enableMotionSensors: Bool = false
@@ -1258,6 +1267,8 @@ struct CraftConfig: Codable {
1258
1267
  var enableScheduledWorkouts: Bool = false
1259
1268
  var devServerURL: String? = nil
1260
1269
  var trustedOrigins: [String] = []
1270
+ /// Domains allowed to back native passkey relying-party identifiers.
1271
+ var associatedDomains: [String] = []
1261
1272
  /// The longest the launch splash stays up when the page neither says it is
1262
1273
  /// ready nor paints.
1263
1274
  var splashMaxSeconds: Double = 3