@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.
- package/dist/app-shortcuts.d.ts +11 -0
- package/dist/cli.js +58 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +65 -1
- package/dist/testing.d.ts +2 -0
- package/package.json +1 -1
- package/templates/CraftApp.swift +392 -88
- package/templates/CraftHeartRate.swift +141 -44
- package/templates/CraftNativeActions.swift +46 -48
- package/templates/CraftNativeScreen.swift +177 -10
- package/templates/CraftPersistentStorage.swift +99 -0
- package/templates/CraftRecordingArchive.swift +85 -0
- package/templates/testing/DriverTests.swift.template +8 -1
|
@@ -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
|
}
|