@craft-native/ios 0.0.236 → 0.0.238

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.
@@ -0,0 +1,11 @@
1
+ export interface CraftAppShortcut {
2
+ id: string;
3
+ title: string;
4
+ subtitle?: string;
5
+ symbol: string;
6
+ url: string;
7
+ /** Each phrase includes exactly one `{app}` token for Siri's app name. */
8
+ phrases: string[];
9
+ }
10
+ /** Build-owned App Intents survive shell restoration and need no app Swift. */
11
+ export declare function appShortcutsSwift(shortcuts?: CraftAppShortcut[], schemes?: string[]): string;
package/dist/cli.js CHANGED
@@ -10,6 +10,60 @@ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync as readdirSync
10
10
  import { dirname as dirname2, extname, join as join3, resolve, sep as sep2 } from "path";
11
11
  var {$ } = globalThis.Bun;
12
12
 
13
+ // src/app-shortcuts.ts
14
+ function literal(value) {
15
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
16
+ }
17
+ function appShortcutsSwift(shortcuts = [], schemes = []) {
18
+ if (shortcuts.length > 10)
19
+ throw new Error("iOS supports at most ten app shortcuts");
20
+ const ids = new Set;
21
+ for (const shortcut of shortcuts) {
22
+ if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(shortcut.id) || ids.has(shortcut.id))
23
+ throw new Error(`Invalid or duplicate app shortcut id: ${shortcut.id}`);
24
+ ids.add(shortcut.id);
25
+ const scheme = /^([a-z][a-z0-9+.-]*):\/\//i.exec(shortcut.url)?.[1];
26
+ if (!scheme || !schemes.includes(scheme) || /[\s\\]/.test(shortcut.url))
27
+ throw new Error(`App shortcut ${shortcut.id} needs a registered app URL scheme`);
28
+ if (!shortcut.title?.trim() || !shortcut.symbol?.trim() || !shortcut.phrases?.length || shortcut.phrases.some((phrase) => phrase.split("{app}").length !== 2))
29
+ throw new Error(`App shortcut ${shortcut.id} needs a title, symbol and phrases with exactly one {app}`);
30
+ }
31
+ const name = (id) => `CraftShortcut${id.split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join("")}Intent`;
32
+ const intents = shortcuts.map((shortcut) => `@available(iOS 16.4, *)
33
+ struct ${name(shortcut.id)}: AppIntent {
34
+ static var title: LocalizedStringResource = ${literal(shortcut.title)}
35
+ static var description = IntentDescription(${literal(shortcut.subtitle ?? shortcut.title)})
36
+ static var openAppWhenRun: Bool = true
37
+ @MainActor
38
+ func perform() async throws -> some IntentResult {
39
+ if let url = URL(string: ${literal(shortcut.url)}) { await UIApplication.shared.open(url) }
40
+ return .result()
41
+ }
42
+ }`).join(`
43
+
44
+ `);
45
+ const entries = shortcuts.map((shortcut) => ` AppShortcut(
46
+ intent: ${name(shortcut.id)}(),
47
+ phrases: [${shortcut.phrases.map((phrase) => literal(phrase).replace("{app}", "\\(.applicationName)")).join(", ")}],
48
+ shortTitle: ${literal(shortcut.title)},
49
+ systemImageName: ${literal(shortcut.symbol)}
50
+ )`).join(`
51
+ `);
52
+ return `// Generated by Craft from appShortcuts. Restored from the package at build time.
53
+ import AppIntents
54
+ import UIKit
55
+
56
+ ${intents}
57
+ ${shortcuts.length ? `
58
+ @available(iOS 16.4, *)
59
+ struct CraftConfiguredAppShortcuts: AppShortcutsProvider {
60
+ static var appShortcuts: [AppShortcut] {
61
+ ${entries}
62
+ }
63
+ }
64
+ ` : ""}`;
65
+ }
66
+
13
67
  // src/source-audit.ts
14
68
  import { readdirSync } from "fs";
15
69
  import { join, relative, sep } from "path";
@@ -890,11 +944,13 @@ function writeShell(output, config) {
890
944
  if (!config.appName || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(config.bundleId))
891
945
  throw new Error("The iOS shell needs an appName and a valid bundleId in craft.config.json.");
892
946
  const name = productName(config.appName);
947
+ const shortcutSource = appShortcutsSwift(config.appShortcuts, config.urlSchemes);
893
948
  mkdirSync(join3(output, "Sources"), { recursive: true });
894
949
  mkdirSync(join3(output, "Shared"), { recursive: true });
895
950
  const swiftTemplate = readFileSync(join3(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
896
951
  const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, config.bundleId).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION);
897
952
  writeFileSync(join3(output, "Sources", `${name}App.swift`), swiftSource);
953
+ writeFileSync(join3(output, "Sources", "CraftAppShortcuts.swift"), shortcutSource);
898
954
  cpSync(join3(TEMPLATES_DIR, "CraftNativeScreen.swift"), join3(output, "Sources", "CraftNativeScreen.swift"));
899
955
  cpSync(join3(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join3(output, "Sources", "CraftNativeFlatList.swift"));
900
956
  cpSync(join3(TEMPLATES_DIR, "CraftNativeActions.swift"), join3(output, "Sources", "CraftNativeActions.swift"));
@@ -902,6 +958,8 @@ function writeShell(output, config) {
902
958
  cpSync(join3(TEMPLATES_DIR, "CraftMusic.swift"), join3(output, "Sources", "CraftMusic.swift"));
903
959
  cpSync(join3(TEMPLATES_DIR, "CraftBluetooth.swift"), join3(output, "Sources", "CraftBluetooth.swift"));
904
960
  cpSync(join3(TEMPLATES_DIR, "CraftHeartRate.swift"), join3(output, "Sources", "CraftHeartRate.swift"));
961
+ cpSync(join3(TEMPLATES_DIR, "CraftRecordingArchive.swift"), join3(output, "Sources", "CraftRecordingArchive.swift"));
962
+ cpSync(join3(TEMPLATES_DIR, "CraftPersistentStorage.swift"), join3(output, "Sources", "CraftPersistentStorage.swift"));
905
963
  cpSync(join3(TEMPLATES_DIR, "CraftScheduledWorkouts.swift"), join3(output, "Sources", "CraftScheduledWorkouts.swift"));
906
964
  cpSync(join3(TEMPLATES_DIR, "CraftNativeMutation.swift"), join3(output, "Sources", "CraftNativeMutation.swift"));
907
965
  cpSync(join3(TEMPLATES_DIR, "CraftNativeUI.swift"), join3(output, "Sources", "CraftNativeUI.swift"));
package/dist/index.d.ts CHANGED
@@ -3,12 +3,14 @@
3
3
  *
4
4
  * Generates native iOS apps from web content using WKWebView.
5
5
  */
6
+ import { type CraftAppShortcut } from './app-shortcuts.js';
6
7
  /** Package custom fonts and their licenses; removed fonts cannot linger in the app. */
7
8
  export declare function syncNativeFonts(fonts: CraftConfig['nativeFonts'], output: string): string[];
8
9
  /** This package's version, which the app names in its user agent (`Craft/<version>`). */
9
10
  export declare function resolveCraftVersion(startDir?: string): string;
10
11
  export type CraftAppearance = 'light' | 'dark' | 'system';
11
12
  export interface CraftConfig {
13
+ appShortcuts?: CraftAppShortcut[];
12
14
  /** Bundled font files keyed by their PostScript font name, used as native fontFamily. */
13
15
  nativeFonts?: Record<string, {
14
16
  src: string;
@@ -272,6 +274,11 @@ export interface CraftTab {
272
274
  title: string;
273
275
  /** An SF Symbol name. */
274
276
  symbol?: string;
277
+ /** Native selected-tab icon overrides; retained pushed screens keep their owning tab's policy. */
278
+ selectedSymbol?: string;
279
+ selectedIconTint?: string;
280
+ /** False suppresses the legacy pulse while selected; omission uses the legacy policy. */
281
+ selectedIconPulse?: boolean;
275
282
  }
276
283
  /** `/a//b/?q#h` → `/a/b`, the way the shell and the page compare paths. Null for a relative path. */
277
284
  export declare function normalizeNativePath(raw: string): string | null;
package/dist/index.js CHANGED
@@ -4,6 +4,60 @@ import { cpSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSy
4
4
  import { dirname as dirname2, extname, join as join3, resolve, sep as sep2 } from "path";
5
5
  var {$ } = globalThis.Bun;
6
6
 
7
+ // src/app-shortcuts.ts
8
+ function literal(value) {
9
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t")}"`;
10
+ }
11
+ function appShortcutsSwift(shortcuts = [], schemes = []) {
12
+ if (shortcuts.length > 10)
13
+ throw new Error("iOS supports at most ten app shortcuts");
14
+ const ids = new Set;
15
+ for (const shortcut of shortcuts) {
16
+ if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(shortcut.id) || ids.has(shortcut.id))
17
+ throw new Error(`Invalid or duplicate app shortcut id: ${shortcut.id}`);
18
+ ids.add(shortcut.id);
19
+ const scheme = /^([a-z][a-z0-9+.-]*):\/\//i.exec(shortcut.url)?.[1];
20
+ if (!scheme || !schemes.includes(scheme) || /[\s\\]/.test(shortcut.url))
21
+ throw new Error(`App shortcut ${shortcut.id} needs a registered app URL scheme`);
22
+ if (!shortcut.title?.trim() || !shortcut.symbol?.trim() || !shortcut.phrases?.length || shortcut.phrases.some((phrase) => phrase.split("{app}").length !== 2))
23
+ throw new Error(`App shortcut ${shortcut.id} needs a title, symbol and phrases with exactly one {app}`);
24
+ }
25
+ const name = (id) => `CraftShortcut${id.split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join("")}Intent`;
26
+ const intents = shortcuts.map((shortcut) => `@available(iOS 16.4, *)
27
+ struct ${name(shortcut.id)}: AppIntent {
28
+ static var title: LocalizedStringResource = ${literal(shortcut.title)}
29
+ static var description = IntentDescription(${literal(shortcut.subtitle ?? shortcut.title)})
30
+ static var openAppWhenRun: Bool = true
31
+ @MainActor
32
+ func perform() async throws -> some IntentResult {
33
+ if let url = URL(string: ${literal(shortcut.url)}) { await UIApplication.shared.open(url) }
34
+ return .result()
35
+ }
36
+ }`).join(`
37
+
38
+ `);
39
+ const entries = shortcuts.map((shortcut) => ` AppShortcut(
40
+ intent: ${name(shortcut.id)}(),
41
+ phrases: [${shortcut.phrases.map((phrase) => literal(phrase).replace("{app}", "\\(.applicationName)")).join(", ")}],
42
+ shortTitle: ${literal(shortcut.title)},
43
+ systemImageName: ${literal(shortcut.symbol)}
44
+ )`).join(`
45
+ `);
46
+ return `// Generated by Craft from appShortcuts. Restored from the package at build time.
47
+ import AppIntents
48
+ import UIKit
49
+
50
+ ${intents}
51
+ ${shortcuts.length ? `
52
+ @available(iOS 16.4, *)
53
+ struct CraftConfiguredAppShortcuts: AppShortcutsProvider {
54
+ static var appShortcuts: [AppShortcut] {
55
+ ${entries}
56
+ }
57
+ }
58
+ ` : ""}`;
59
+ }
60
+
7
61
  // src/source-audit.ts
8
62
  import { readdirSync } from "fs";
9
63
  import { join, relative, sep } from "path";
@@ -79,13 +133,19 @@ function initSimulatorDriver(options) {
79
133
  throw new Error("The simulator driver requires iOS 17.0 or newer");
80
134
  if (!["http:", "https:"].includes(new URL(control).protocol))
81
135
  throw new Error("controlUrl must use HTTP or HTTPS");
136
+ const tabIdentifiers = Object.entries(options.tabIdentifiers ?? {});
137
+ for (const [name2, identifier] of tabIdentifiers) {
138
+ if (!name2.trim() || !identifier.trim())
139
+ throw new Error("Tab names and native identifiers must be nonempty");
140
+ }
82
141
  const replacements = {
83
142
  PROJECT_NAME: name,
84
143
  BUNDLE_PREFIX: prefix,
85
144
  IOS_VERSION: version,
86
145
  BUNDLE_ID: swiftString(bundleId),
87
146
  CONTROL_URL: swiftString(control),
88
- TABS: `[${(options.tabs ?? []).map(swiftString).join(", ")}]`
147
+ TABS: `[${(options.tabs ?? []).map(swiftString).join(", ")}]`,
148
+ TAB_IDENTIFIERS: tabIdentifiers.length ? `[${tabIdentifiers.map(([name2, identifier]) => `${swiftString(name2)}: ${swiftString(identifier)}`).join(", ")}]` : "[:]"
89
149
  };
90
150
  for (const [source, destination] of [
91
151
  ["HostApp.swift", "Host/HostApp.swift"],
@@ -1073,11 +1133,13 @@ function writeShell(output, config) {
1073
1133
  if (!config.appName || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(config.bundleId))
1074
1134
  throw new Error("The iOS shell needs an appName and a valid bundleId in craft.config.json.");
1075
1135
  const name = productName(config.appName);
1136
+ const shortcutSource = appShortcutsSwift(config.appShortcuts, config.urlSchemes);
1076
1137
  mkdirSync2(join3(output, "Sources"), { recursive: true });
1077
1138
  mkdirSync2(join3(output, "Shared"), { recursive: true });
1078
1139
  const swiftTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
1079
1140
  const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, config.bundleId).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION);
1080
1141
  writeFileSync2(join3(output, "Sources", `${name}App.swift`), swiftSource);
1142
+ writeFileSync2(join3(output, "Sources", "CraftAppShortcuts.swift"), shortcutSource);
1081
1143
  cpSync(join3(TEMPLATES_DIR, "CraftNativeScreen.swift"), join3(output, "Sources", "CraftNativeScreen.swift"));
1082
1144
  cpSync(join3(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join3(output, "Sources", "CraftNativeFlatList.swift"));
1083
1145
  cpSync(join3(TEMPLATES_DIR, "CraftNativeActions.swift"), join3(output, "Sources", "CraftNativeActions.swift"));
@@ -1085,6 +1147,8 @@ function writeShell(output, config) {
1085
1147
  cpSync(join3(TEMPLATES_DIR, "CraftMusic.swift"), join3(output, "Sources", "CraftMusic.swift"));
1086
1148
  cpSync(join3(TEMPLATES_DIR, "CraftBluetooth.swift"), join3(output, "Sources", "CraftBluetooth.swift"));
1087
1149
  cpSync(join3(TEMPLATES_DIR, "CraftHeartRate.swift"), join3(output, "Sources", "CraftHeartRate.swift"));
1150
+ cpSync(join3(TEMPLATES_DIR, "CraftRecordingArchive.swift"), join3(output, "Sources", "CraftRecordingArchive.swift"));
1151
+ cpSync(join3(TEMPLATES_DIR, "CraftPersistentStorage.swift"), join3(output, "Sources", "CraftPersistentStorage.swift"));
1088
1152
  cpSync(join3(TEMPLATES_DIR, "CraftScheduledWorkouts.swift"), join3(output, "Sources", "CraftScheduledWorkouts.swift"));
1089
1153
  cpSync(join3(TEMPLATES_DIR, "CraftNativeMutation.swift"), join3(output, "Sources", "CraftNativeMutation.swift"));
1090
1154
  cpSync(join3(TEMPLATES_DIR, "CraftNativeUI.swift"), join3(output, "Sources", "CraftNativeUI.swift"));
package/dist/testing.d.ts CHANGED
@@ -7,6 +7,8 @@ export interface SimulatorDriverOptions {
7
7
  deploymentTarget?: string;
8
8
  controlUrl?: string;
9
9
  tabs?: readonly string[];
10
+ /** Stable native accessibility identifiers for tabs whose labels can change. */
11
+ tabIdentifiers?: Readonly<Record<string, string>>;
10
12
  /** Bundle a wire-protocol journey and save native XCTest attachments. No control server is needed on physical devices. */
11
13
  script?: string;
12
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-native/ios",
3
- "version": "0.0.236",
3
+ "version": "0.0.238",
4
4
  "homepage": "https://github.com/craft-native/craft/tree/main/packages/ios#readme",
5
5
  "repository": {
6
6
  "type": "git",