craft-native 0.0.55 → 0.0.74

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.
Files changed (146) hide show
  1. package/dist/android/src/index.d.ts +73 -0
  2. package/dist/android/src/index.js +177 -37
  3. package/dist/android/templates/AndroidManifest.xml.template +10 -36
  4. package/dist/android/templates/CraftBridge.kt.template +192 -60
  5. package/dist/android/templates/CraftHealthConnect.kt.template +200 -0
  6. package/dist/android/templates/CraftHealthConnectStub.kt.template +28 -0
  7. package/dist/android/templates/LocationRecordingService.kt.template +160 -0
  8. package/dist/android/templates/MainActivity.kt.template +140 -19
  9. package/dist/android/templates/build.gradle.kts.app.template +23 -1
  10. package/dist/android/templates/build.gradle.kts.project.template +3 -2
  11. package/dist/android/templates/proguard-rules.pro.template +9 -0
  12. package/dist/api/android-advanced.d.ts +5 -6
  13. package/dist/api/app.d.ts +0 -1
  14. package/dist/api/clipboard.d.ts +0 -1
  15. package/dist/api/crypto.d.ts +1 -2
  16. package/dist/api/db.d.ts +1 -2
  17. package/dist/api/dialog.d.ts +0 -1
  18. package/dist/api/fs.d.ts +1 -2
  19. package/dist/api/gestures.d.ts +66 -0
  20. package/dist/api/http.d.ts +1 -2
  21. package/dist/api/index.d.ts +37 -34
  22. package/dist/api/ios-advanced.d.ts +6 -7
  23. package/dist/api/macos-advanced.d.ts +2 -3
  24. package/dist/api/media.d.ts +0 -1
  25. package/dist/api/mobile.d.ts +109 -1
  26. package/dist/api/process.d.ts +0 -1
  27. package/dist/api/sidebar.d.ts +3 -20
  28. package/dist/api/spaces.d.ts +83 -0
  29. package/dist/api/tray.d.ts +2 -3
  30. package/dist/api/window.d.ts +1 -2
  31. package/dist/api/windows-advanced.d.ts +6 -7
  32. package/dist/assets/index.d.ts +0 -1
  33. package/dist/binary-resolver.d.ts +23 -1
  34. package/dist/bridge/android.d.ts +0 -1
  35. package/dist/bridge/core.d.ts +0 -38
  36. package/dist/bridge/ids.d.ts +0 -1
  37. package/dist/bridge/ios.d.ts +0 -1
  38. package/dist/bridge/webkit-pending.d.ts +0 -1
  39. package/dist/build/bundle-optimizer.d.ts +0 -1
  40. package/dist/cli.js +666 -90
  41. package/dist/components/index.d.ts +6 -7
  42. package/dist/components/native.d.ts +0 -1
  43. package/dist/components/sidebar.d.ts +2 -81
  44. package/dist/dev/devtools.d.ts +0 -1
  45. package/dist/dev/error-overlay.d.ts +0 -1
  46. package/dist/dev/hot-reload.d.ts +0 -1
  47. package/dist/index.cjs +113 -3
  48. package/dist/index.d.cts +16 -16
  49. package/dist/index.d.ts +16 -16
  50. package/dist/index.js +112 -4
  51. package/dist/ios/src/index.d.ts +172 -0
  52. package/dist/ios/src/index.js +440 -42
  53. package/dist/ios/templates/CraftActivityAttributes.swift +14 -0
  54. package/dist/ios/templates/CraftApp.swift +1001 -73
  55. package/dist/ios/templates/CraftLiveActivityWidget.swift.template +62 -0
  56. package/dist/ios/templates/CraftWatchApp.swift.template +155 -0
  57. package/dist/ios/templates/Info.plist.template +10 -9
  58. package/dist/ios/templates/WatchApp.Info.plist.template +26 -0
  59. package/dist/ios/templates/WidgetExtension.Info.plist +31 -0
  60. package/dist/ios/templates/project.yml.template +12 -1
  61. package/dist/logging/index.d.ts +0 -1
  62. package/dist/mobile.d.ts +8 -0
  63. package/dist/mobile.js +755 -0
  64. package/dist/optimizations/index.d.ts +3 -4
  65. package/dist/package.d.ts +11 -1
  66. package/dist/performance/runtime.d.ts +0 -1
  67. package/dist/performance/startup.d.ts +0 -1
  68. package/dist/plugins/index.d.ts +0 -1
  69. package/dist/security/index.d.ts +0 -1
  70. package/dist/sidebar-types.d.ts +102 -0
  71. package/dist/signing/index.d.ts +0 -1
  72. package/dist/styles/headwind.d.ts +0 -1
  73. package/dist/styles/index.d.ts +9 -10
  74. package/dist/styles/sidebar-templates.d.ts +0 -1
  75. package/dist/styles/sidebars.d.ts +0 -1
  76. package/dist/types.d.ts +131 -1
  77. package/dist/updater/index.d.ts +0 -1
  78. package/dist/utils/audio.d.ts +0 -1
  79. package/dist/utils/index.d.ts +4 -5
  80. package/dist/utils/native.d.ts +1 -2
  81. package/dist/utils/react.d.ts +0 -1
  82. package/dist/utils/storage.d.ts +0 -1
  83. package/dist/utils/svelte.d.ts +0 -1
  84. package/dist/utils/timer.d.ts +0 -1
  85. package/dist/utils/vue.d.ts +0 -1
  86. package/package.json +32 -4
  87. package/dist/android/src/index.js.map +0 -10
  88. package/dist/api/android-advanced.d.ts.map +0 -1
  89. package/dist/api/app.d.ts.map +0 -1
  90. package/dist/api/clipboard.d.ts.map +0 -1
  91. package/dist/api/crypto.d.ts.map +0 -1
  92. package/dist/api/db.d.ts.map +0 -1
  93. package/dist/api/dialog.d.ts.map +0 -1
  94. package/dist/api/fs.d.ts.map +0 -1
  95. package/dist/api/http.d.ts.map +0 -1
  96. package/dist/api/index.d.ts.map +0 -1
  97. package/dist/api/ios-advanced.d.ts.map +0 -1
  98. package/dist/api/macos-advanced.d.ts.map +0 -1
  99. package/dist/api/media.d.ts.map +0 -1
  100. package/dist/api/mobile.d.ts.map +0 -1
  101. package/dist/api/process.d.ts.map +0 -1
  102. package/dist/api/sidebar.d.ts.map +0 -1
  103. package/dist/api/tray.d.ts.map +0 -1
  104. package/dist/api/window.d.ts.map +0 -1
  105. package/dist/api/windows-advanced.d.ts.map +0 -1
  106. package/dist/assets/index.d.ts.map +0 -1
  107. package/dist/binary-resolver.d.ts.map +0 -1
  108. package/dist/bridge/android.d.ts.map +0 -1
  109. package/dist/bridge/core.d.ts.map +0 -1
  110. package/dist/bridge/ids.d.ts.map +0 -1
  111. package/dist/bridge/ios.d.ts.map +0 -1
  112. package/dist/bridge/webkit-pending.d.ts.map +0 -1
  113. package/dist/build/bundle-optimizer.d.ts.map +0 -1
  114. package/dist/components/index.d.ts.map +0 -1
  115. package/dist/components/native.d.ts.map +0 -1
  116. package/dist/components/sidebar.d.ts.map +0 -1
  117. package/dist/dev/devtools.d.ts.map +0 -1
  118. package/dist/dev/error-overlay.d.ts.map +0 -1
  119. package/dist/dev/hot-reload.d.ts.map +0 -1
  120. package/dist/index.d.ts.map +0 -1
  121. package/dist/index.js.map +0 -45
  122. package/dist/ios/src/index.js.map +0 -10
  123. package/dist/logging/index.d.ts.map +0 -1
  124. package/dist/optimizations/index.d.ts.map +0 -1
  125. package/dist/package.d.ts.map +0 -1
  126. package/dist/package.test.d.ts +0 -2
  127. package/dist/package.test.d.ts.map +0 -1
  128. package/dist/performance/runtime.d.ts.map +0 -1
  129. package/dist/performance/startup.d.ts.map +0 -1
  130. package/dist/plugins/index.d.ts.map +0 -1
  131. package/dist/security/index.d.ts.map +0 -1
  132. package/dist/signing/index.d.ts.map +0 -1
  133. package/dist/styles/headwind.d.ts.map +0 -1
  134. package/dist/styles/index.d.ts.map +0 -1
  135. package/dist/styles/sidebar-templates.d.ts.map +0 -1
  136. package/dist/styles/sidebars.d.ts.map +0 -1
  137. package/dist/types.d.ts.map +0 -1
  138. package/dist/updater/index.d.ts.map +0 -1
  139. package/dist/utils/audio.d.ts.map +0 -1
  140. package/dist/utils/index.d.ts.map +0 -1
  141. package/dist/utils/native.d.ts.map +0 -1
  142. package/dist/utils/react.d.ts.map +0 -1
  143. package/dist/utils/storage.d.ts.map +0 -1
  144. package/dist/utils/svelte.d.ts.map +0 -1
  145. package/dist/utils/timer.d.ts.map +0 -1
  146. package/dist/utils/vue.d.ts.map +0 -1
@@ -1,16 +1,293 @@
1
1
  // @bun
2
2
  // ../ios/src/index.ts
3
- import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from "fs";
4
- import { join, dirname } from "path";
3
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
4
+ import { dirname, join, resolve } from "path";
5
5
  var {$ } = globalThis.Bun;
6
6
  var TEMPLATES_DIR = join(dirname(import.meta.dir), "templates");
7
+ var DEFAULT_CONFIG = {
8
+ version: "1.0.0",
9
+ buildNumber: "1",
10
+ darkMode: true,
11
+ backgroundColor: "#0b1712",
12
+ enableSpeechRecognition: false,
13
+ enableHaptics: false,
14
+ enableShare: false,
15
+ enableCamera: false,
16
+ enableBiometric: false,
17
+ enablePushNotifications: false,
18
+ enableSecureStorage: false,
19
+ enableGeolocation: false,
20
+ enableClipboard: false,
21
+ enableContacts: false,
22
+ enableCalendar: false,
23
+ enableLocalNotifications: false,
24
+ enableInAppPurchase: false,
25
+ enableKeepAwake: false,
26
+ enableOrientationLock: false,
27
+ enableDeepLinks: false,
28
+ enableQRScanner: false,
29
+ enableFilePicker: false,
30
+ enableFileDownload: false,
31
+ enableSocialAuth: false,
32
+ enableAudioRecording: false,
33
+ enableVideoRecording: false,
34
+ enableMotionSensors: false,
35
+ enableLocalDatabase: false,
36
+ enableBluetooth: false,
37
+ enableNFC: false,
38
+ enableHealthKit: false,
39
+ enableLiveActivities: false,
40
+ enableWatchApp: false,
41
+ enableBackgroundLocation: false,
42
+ enableBackgroundTasks: false,
43
+ enableScreenCapture: false,
44
+ enablePDFViewer: false,
45
+ enableAR: false,
46
+ enableMLKit: false,
47
+ iosVersion: "16.0",
48
+ watchosVersion: "9.0",
49
+ teamId: "",
50
+ trustedOrigins: [],
51
+ associatedDomains: [],
52
+ appGroups: [],
53
+ orientations: ["portrait"],
54
+ deviceFamilies: ["iphone", "ipad"]
55
+ };
56
+ function renderDeviceFamilies(config) {
57
+ const values = new Set(((config.deviceFamilies?.length) ? config.deviceFamilies : ["iphone", "ipad"]).map((family) => family === "ipad" ? "2" : "1"));
58
+ return [...values].join(",");
59
+ }
60
+ function xmlEscape(value) {
61
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll(/\u0027/g, "&apos;");
62
+ }
63
+ function plistString(key, value) {
64
+ return ` <key>${key}</key>
65
+ <string>${xmlEscape(value)}</string>`;
66
+ }
67
+ function renderUsageDescriptions(config) {
68
+ const entries = [
69
+ [config.enableSpeechRecognition, "NSSpeechRecognitionUsageDescription", `${config.appName} uses speech recognition for voice input.`],
70
+ [config.enableSpeechRecognition || config.enableAudioRecording, "NSMicrophoneUsageDescription", `${config.appName} uses the microphone to record audio.`],
71
+ [config.enableCamera || config.enableVideoRecording || config.enableQRScanner || config.enableAR, "NSCameraUsageDescription", `${config.appName} uses the camera for photos, video, scanning, and augmented reality.`],
72
+ [config.enableCamera || config.enableVideoRecording, "NSPhotoLibraryUsageDescription", `${config.appName} lets you choose photos and videos from your library.`],
73
+ [config.enableGeolocation || config.enableBackgroundLocation, "NSLocationWhenInUseUsageDescription", `${config.appName} uses your location while you record an activity.`],
74
+ [config.enableBackgroundLocation, "NSLocationAlwaysAndWhenInUseUsageDescription", `${config.appName} continues recording your route when the screen is locked or the app is in the background.`],
75
+ [config.enableContacts, "NSContactsUsageDescription", `${config.appName} accesses contacts only when you choose a contact feature.`],
76
+ [config.enableCalendar, "NSCalendarsUsageDescription", `${config.appName} accesses your calendar only when you choose a calendar feature.`],
77
+ [config.enableBluetooth, "NSBluetoothAlwaysUsageDescription", `${config.appName} uses Bluetooth to connect to nearby devices.`],
78
+ [config.enableMotionSensors, "NSMotionUsageDescription", `${config.appName} uses motion data for activity features.`],
79
+ [config.enableNFC, "NFCReaderUsageDescription", `${config.appName} reads NFC tags when you start a scan.`],
80
+ [config.enableHealthKit, "NSHealthShareUsageDescription", `${config.appName} reads health data you choose to share.`],
81
+ [config.enableHealthKit, "NSHealthUpdateUsageDescription", `${config.appName} writes health data only with your permission.`],
82
+ [config.enableBiometric, "NSFaceIDUsageDescription", `${config.appName} uses Face ID to protect your account.`]
83
+ ];
84
+ return entries.filter(([enabled]) => enabled).map(([, key, value]) => plistString(key, value)).join(`
85
+ `);
86
+ }
87
+ function renderOrientations(config) {
88
+ const names = {
89
+ portrait: "UIInterfaceOrientationPortrait",
90
+ "landscape-left": "UIInterfaceOrientationLandscapeLeft",
91
+ "landscape-right": "UIInterfaceOrientationLandscapeRight",
92
+ "portrait-upside-down": "UIInterfaceOrientationPortraitUpsideDown"
93
+ };
94
+ const values = config.orientations?.length ? config.orientations : ["portrait"];
95
+ return values.map((value) => ` <string>${names[value]}</string>`).join(`
96
+ `);
97
+ }
98
+ function renderUrlTypes(config) {
99
+ const schemes = [...new Set(config.urlSchemes?.map((value) => value.trim()).filter(Boolean) ?? [])];
100
+ if (!schemes.length)
101
+ return "";
102
+ return ` <key>CFBundleURLTypes</key>
103
+ <array>
104
+ <dict>
105
+ <key>CFBundleURLSchemes</key>
106
+ <array>
107
+ ${schemes.map((value) => ` <string>${xmlEscape(value)}</string>`).join(`
108
+ `)}
109
+ </array>
110
+ </dict>
111
+ </array>`;
112
+ }
113
+ function plistArray(values, indent = 2) {
114
+ const padding = " ".repeat(indent);
115
+ return values.map((value) => `${padding}<string>${xmlEscape(value)}</string>`).join(`
116
+ `);
117
+ }
118
+ function renderBackgroundModes(config) {
119
+ const modes = new Set;
120
+ if (config.enableBackgroundLocation)
121
+ modes.add("location");
122
+ if (config.enableBackgroundTasks)
123
+ modes.add("processing");
124
+ if (config.enablePushNotifications)
125
+ modes.add("remote-notification");
126
+ if (!modes.size)
127
+ return "";
128
+ return ` <key>UIBackgroundModes</key>
129
+ <array>
130
+ ${plistArray([...modes])}
131
+ </array>`;
132
+ }
133
+ function renderEntitlements(config) {
134
+ const entries = [];
135
+ if (config.associatedDomains?.length) {
136
+ entries.push(` <key>com.apple.developer.associated-domains</key>
137
+ <array>
138
+ ${plistArray(config.associatedDomains)}
139
+ </array>`);
140
+ }
141
+ if (config.appGroups?.length) {
142
+ entries.push(` <key>com.apple.security.application-groups</key>
143
+ <array>
144
+ ${plistArray(config.appGroups)}
145
+ </array>`);
146
+ }
147
+ if (config.enableHealthKit) {
148
+ entries.push(` <key>com.apple.developer.healthkit</key>
149
+ <true/>`);
150
+ }
151
+ if (config.enablePushNotifications) {
152
+ entries.push(` <key>aps-environment</key>
153
+ <string>development</string>`);
154
+ }
155
+ return `<?xml version="1.0" encoding="UTF-8"?>
156
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
157
+ <plist version="1.0">
158
+ <dict>
159
+ ${entries.join(`
160
+ `)}
161
+ </dict>
162
+ </plist>
163
+ `;
164
+ }
165
+ function renderWatchEntitlements(config) {
166
+ const appGroups = config.appGroups?.length ? ` <key>com.apple.security.application-groups</key>
167
+ <array>
168
+ ${plistArray(config.appGroups)}
169
+ </array>
170
+ ` : "";
171
+ return `<?xml version="1.0" encoding="UTF-8"?>
172
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
173
+ <plist version="1.0">
174
+ <dict>
175
+ ${appGroups}</dict>
176
+ </plist>
177
+ `;
178
+ }
179
+ function renderPrivacyManifest(config) {
180
+ const privacy = config.privacy ?? {};
181
+ const collected = privacy.collectedDataTypes ?? [];
182
+ const accessed = privacy.accessedApiTypes ?? [];
183
+ const collectedXml = collected.map((item) => ` <dict>
184
+ <key>NSPrivacyCollectedDataType</key>
185
+ <string>${xmlEscape(item.type)}</string>
186
+ <key>NSPrivacyCollectedDataTypeLinked</key>
187
+ <${item.linked ? "true" : "false"}/>
188
+ <key>NSPrivacyCollectedDataTypeTracking</key>
189
+ <${item.tracking ? "true" : "false"}/>
190
+ <key>NSPrivacyCollectedDataTypePurposes</key>
191
+ <array>
192
+ ${plistArray(item.purposes, 4)}
193
+ </array>
194
+ </dict>`).join(`
195
+ `);
196
+ const accessedXml = accessed.map((item) => ` <dict>
197
+ <key>NSPrivacyAccessedAPIType</key>
198
+ <string>${xmlEscape(item.type)}</string>
199
+ <key>NSPrivacyAccessedAPITypeReasons</key>
200
+ <array>
201
+ ${plistArray(item.reasons, 4)}
202
+ </array>
203
+ </dict>`).join(`
204
+ `);
205
+ return `<?xml version="1.0" encoding="UTF-8"?>
206
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
207
+ <plist version="1.0">
208
+ <dict>
209
+ <key>NSPrivacyTracking</key>
210
+ <${privacy.tracking ? "true" : "false"}/>
211
+ <key>NSPrivacyTrackingDomains</key>
212
+ <array>
213
+ ${plistArray(privacy.trackingDomains ?? [])}
214
+ </array>
215
+ <key>NSPrivacyCollectedDataTypes</key>
216
+ <array>
217
+ ${collectedXml}
218
+ </array>
219
+ <key>NSPrivacyAccessedAPITypes</key>
220
+ <array>
221
+ ${accessedXml}
222
+ </array>
223
+ </dict>
224
+ </plist>
225
+ `;
226
+ }
227
+ function renderAssetCatalog(output, config) {
228
+ const catalog = join(output, "Assets.xcassets");
229
+ const appIcon = join(catalog, "AppIcon.appiconset");
230
+ const launchBackground = join(catalog, "LaunchBackground.colorset");
231
+ mkdirSync(appIcon, { recursive: true });
232
+ mkdirSync(launchBackground, { recursive: true });
233
+ writeFileSync(join(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
234
+ `);
235
+ const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
236
+ if (config.appIconPath) {
237
+ if (!existsSync(config.appIconPath))
238
+ throw new Error(`App icon not found: ${config.appIconPath}`);
239
+ cpSync(config.appIconPath, join(appIcon, iconFilename));
240
+ }
241
+ writeFileSync(join(appIcon, "Contents.json"), `${JSON.stringify({
242
+ images: iconFilename ? [{
243
+ filename: iconFilename,
244
+ idiom: "universal",
245
+ platform: "ios",
246
+ size: "1024x1024"
247
+ }] : [],
248
+ info: { author: "xcode", version: 1 }
249
+ }, null, 2)}
250
+ `);
251
+ const color = config.backgroundColor?.replace(/^#/, "") || "000000";
252
+ const normalized = color.length === 3 ? [...color].map((value) => `${value}${value}`).join("") : color.padEnd(6, "0").slice(0, 6);
253
+ const components = [0, 2, 4].map((index) => (Number.parseInt(normalized.slice(index, index + 2), 16) / 255).toFixed(3));
254
+ writeFileSync(join(launchBackground, "Contents.json"), `${JSON.stringify({
255
+ colors: [{
256
+ color: {
257
+ "color-space": "srgb",
258
+ components: { alpha: "1.000", blue: components[2], green: components[1], red: components[0] }
259
+ },
260
+ idiom: "universal"
261
+ }],
262
+ info: { author: "xcode", version: 1 }
263
+ }, null, 2)}
264
+ `);
265
+ }
266
+ function syncWebAssets(source, output) {
267
+ const sourcePath = resolve(source);
268
+ if (!existsSync(sourcePath))
269
+ throw new Error(`Web asset path not found: ${source}`);
270
+ const distDir = join(output, "dist");
271
+ rmSync(distDir, { recursive: true, force: true });
272
+ mkdirSync(distDir, { recursive: true });
273
+ if (statSync(sourcePath).isDirectory()) {
274
+ cpSync(sourcePath, distDir, { recursive: true });
275
+ } else {
276
+ cpSync(sourcePath, join(distDir, "index.html"));
277
+ }
278
+ if (!existsSync(join(distDir, "index.html"))) {
279
+ throw new Error(`Web asset directory must contain index.html: ${source}`);
280
+ }
281
+ }
7
282
  async function init(options) {
8
283
  const { name, bundleId, teamId, output } = options;
9
284
  console.log(`
10
285
  \u26A1 Initializing Craft iOS project: ${name}`);
11
286
  console.log(` Output: ${output}
12
287
  `);
13
- const dirs = [output, join(output, "Sources"), join(output, "dist")];
288
+ const dirs = [output, join(output, "Sources"), join(output, "Shared"), join(output, "dist")];
289
+ if (options.config?.enableWatchApp)
290
+ dirs.push(join(output, "WatchApp"));
14
291
  for (const dir of dirs) {
15
292
  if (!existsSync(dir)) {
16
293
  mkdirSync(dir, { recursive: true });
@@ -19,27 +296,84 @@ async function init(options) {
19
296
  const finalBundleId = bundleId || `com.craft.${name.toLowerCase().replace(/[^a-z0-9]/g, "")}`;
20
297
  const bundleIdPrefix = finalBundleId.split(".").slice(0, -1).join(".");
21
298
  const config = {
299
+ ...DEFAULT_CONFIG,
22
300
  appName: name,
23
301
  bundleId: finalBundleId,
24
- version: "1.0.0",
25
- buildNumber: "1",
26
- darkMode: true,
27
- backgroundColor: "#1a1a2e",
28
- enableSpeechRecognition: true,
29
- enableHaptics: true,
30
- enableShare: true,
31
- iosVersion: "15.0",
32
- teamId: teamId || ""
302
+ teamId: teamId || "",
303
+ ...options.config
33
304
  };
305
+ if (config.enableBackgroundLocation)
306
+ config.enableGeolocation = true;
34
307
  writeFileSync(join(output, "craft.config.json"), JSON.stringify(config, null, 2));
35
308
  const swiftTemplate = readFileSync(join(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
36
- writeFileSync(join(output, "Sources", `${name}App.swift`), swiftTemplate.replace(/CraftApp/g, `${name}App`));
309
+ const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
310
+ writeFileSync(join(output, "Sources", `${name}App.swift`), swiftSource);
37
311
  const infoPlistTemplate = readFileSync(join(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
38
- const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, config.darkMode ? "Dark" : "Light");
312
+ const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, config.darkMode ? "Dark" : "Light").replace(/\{\{ORIENTATIONS\}\}/g, renderOrientations(config)).replace(/\{\{USAGE_DESCRIPTIONS\}\}/g, renderUsageDescriptions(config)).replace(/\{\{URL_TYPES\}\}/g, renderUrlTypes(config)).replace(/\{\{BACKGROUND_MODES\}\}/g, renderBackgroundModes(config)).replace(/\{\{LIVE_ACTIVITY_SUPPORT\}\}/g, config.enableLiveActivities ? ` <key>NSSupportsLiveActivities</key>
313
+ <true/>
314
+ <key>NSSupportsLiveActivitiesFrequentUpdates</key>
315
+ <true/>` : "");
39
316
  writeFileSync(join(output, "Info.plist"), infoPlist);
40
317
  const projectYmlTemplate = readFileSync(join(TEMPLATES_DIR, "project.yml.template"), "utf-8");
41
- const projectYml = projectYmlTemplate.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(/\{\{TEAM_ID\}\}/g, teamId || "");
318
+ const nativeDependencies = [];
319
+ if (config.enableLiveActivities)
320
+ nativeDependencies.push(` - target: ${name}LiveActivity`);
321
+ if (config.enableWatchApp)
322
+ nativeDependencies.push(` - target: ${name}Watch
323
+ embed: true`);
324
+ const nativeTargets = [];
325
+ if (config.enableLiveActivities) {
326
+ nativeTargets.push(` ${name}LiveActivity:
327
+ type: app-extension
328
+ platform: iOS
329
+ deploymentTarget: "16.2"
330
+ sources:
331
+ - WidgetExtension
332
+ - Shared
333
+ settings:
334
+ INFOPLIST_FILE: WidgetExtension/Info.plist
335
+ PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.liveactivity
336
+ SWIFT_VERSION: "5.0"
337
+ APPLICATION_EXTENSION_API_ONLY: YES
338
+ SKIP_INSTALL: YES`);
339
+ }
340
+ if (config.enableWatchApp) {
341
+ nativeTargets.push(` ${name}Watch:
342
+ type: application
343
+ platform: watchOS
344
+ deploymentTarget: "${config.watchosVersion || "9.0"}"
345
+ sources:
346
+ - WatchApp
347
+ settings:
348
+ INFOPLIST_FILE: WatchApp/Info.plist
349
+ CODE_SIGN_ENTITLEMENTS: WatchApp/Watch.entitlements
350
+ PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.watchkitapp
351
+ SWIFT_VERSION: "5.0"
352
+ SKIP_INSTALL: YES`);
353
+ }
354
+ const projectYml = projectYmlTemplate.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:
355
+ ${nativeDependencies.join(`
356
+ `)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
357
+ `));
42
358
  writeFileSync(join(output, "project.yml"), projectYml);
359
+ writeFileSync(join(output, "Craft.entitlements"), renderEntitlements(config));
360
+ writeFileSync(join(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
361
+ renderAssetCatalog(output, config);
362
+ cpSync(join(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join(output, "Shared", "CraftActivityAttributes.swift"));
363
+ if (config.enableLiveActivities) {
364
+ mkdirSync(join(output, "WidgetExtension"), { recursive: true });
365
+ const widgetSource = readFileSync(join(TEMPLATES_DIR, "CraftLiveActivityWidget.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
366
+ writeFileSync(join(output, "WidgetExtension", `${name}LiveActivity.swift`), widgetSource);
367
+ const widgetInfo = readFileSync(join(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
368
+ writeFileSync(join(output, "WidgetExtension", "Info.plist"), widgetInfo);
369
+ }
370
+ if (config.enableWatchApp) {
371
+ const watchSource = readFileSync(join(TEMPLATES_DIR, "CraftWatchApp.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
372
+ writeFileSync(join(output, "WatchApp", `${name}WatchApp.swift`), watchSource);
373
+ const watchInfo = readFileSync(join(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
374
+ writeFileSync(join(output, "WatchApp", "Info.plist"), watchInfo);
375
+ writeFileSync(join(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
376
+ }
43
377
  const placeholderHtml = `<!DOCTYPE html>
44
378
  <html>
45
379
  <head>
@@ -89,7 +423,7 @@ async function init(options) {
89
423
  console.log("");
90
424
  }
91
425
  async function build(options) {
92
- const { htmlPath, devServer, output } = options;
426
+ const { htmlPath, devServer, output, generateProject = true } = options;
93
427
  console.log(`
94
428
  \uD83D\uDCE6 Building Craft iOS project...`);
95
429
  const configPath = join(output, "craft.config.json");
@@ -99,25 +433,17 @@ async function build(options) {
99
433
  const config = JSON.parse(readFileSync(configPath, "utf-8"));
100
434
  if (devServer) {
101
435
  config.devServerURL = devServer;
436
+ const origin = new URL(devServer).origin;
437
+ config.trustedOrigins = [...new Set([...config.trustedOrigins ?? [], origin])];
102
438
  writeFileSync(configPath, JSON.stringify(config, null, 2));
103
439
  console.log(` Dev server: ${devServer}`);
104
440
  }
105
441
  if (htmlPath) {
106
- const distDir = join(output, "dist");
107
- if (!existsSync(distDir)) {
108
- mkdirSync(distDir, { recursive: true });
109
- }
110
- if (existsSync(htmlPath)) {
111
- const stat = await Bun.file(htmlPath).exists();
112
- if (stat) {
113
- const html = readFileSync(htmlPath, "utf-8");
114
- writeFileSync(join(distDir, "index.html"), html);
115
- console.log(` Copied: ${htmlPath} \u2192 dist/index.html`);
116
- }
117
- } else {
118
- throw new Error(`HTML path not found: ${htmlPath}`);
119
- }
442
+ syncWebAssets(htmlPath, output);
443
+ console.log(` Synced: ${htmlPath} \u2192 dist/`);
120
444
  }
445
+ if (!generateProject)
446
+ return;
121
447
  try {
122
448
  const result = await $`which xcodegen`.quiet();
123
449
  if (result.exitCode === 0) {
@@ -127,10 +453,10 @@ async function build(options) {
127
453
  } else {
128
454
  throw new Error("xcodegen not found");
129
455
  }
130
- } catch {
131
- console.log("\u26A0\uFE0F xcodegen not found. Install with: brew install xcodegen");
132
- console.log(" Then run: craft ios build");
133
- return;
456
+ } catch (error) {
457
+ throw new Error("Unable to generate the Xcode project. Install xcodegen with `brew install xcodegen`.", {
458
+ cause: error
459
+ });
134
460
  }
135
461
  console.log("");
136
462
  }
@@ -145,6 +471,51 @@ async function open(options) {
145
471
  console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
146
472
  await $`open ${projectPath}`;
147
473
  }
474
+ function orderSimulators(devices) {
475
+ return [...devices].sort((left, right) => {
476
+ const booted = Number(right.state === "Booted") - Number(left.state === "Booted");
477
+ if (booted !== 0)
478
+ return booted;
479
+ const phone = Number(right.name.startsWith("iPhone")) - Number(left.name.startsWith("iPhone"));
480
+ if (phone !== 0)
481
+ return phone;
482
+ return right.runtime.localeCompare(left.runtime);
483
+ });
484
+ }
485
+ async function pickSimulator() {
486
+ const listed = await $`xcrun simctl list devices available --json`.quiet().nothrow();
487
+ if (listed.exitCode !== 0)
488
+ return null;
489
+ let parsed;
490
+ try {
491
+ parsed = JSON.parse(listed.stdout.toString());
492
+ } catch {
493
+ return null;
494
+ }
495
+ const devices = [];
496
+ for (const [runtime, list] of Object.entries(parsed.devices ?? {})) {
497
+ for (const device of list)
498
+ devices.push({ ...device, runtime: runtime.split(".").pop() ?? runtime });
499
+ }
500
+ return orderSimulators(devices)[0] ?? null;
501
+ }
502
+ async function bootSimulator(device) {
503
+ if (device.state !== "Booted")
504
+ await $`xcrun simctl boot ${device.udid}`.quiet().nothrow();
505
+ await $`xcrun simctl bootstatus ${device.udid}`.quiet().nothrow();
506
+ }
507
+ async function showSimulator() {
508
+ const selected = await $`xcode-select -p`.quiet().nothrow();
509
+ const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
510
+ if (developer) {
511
+ const app = join(developer, "Applications", "Simulator.app");
512
+ if (existsSync(app)) {
513
+ await $`open ${app}`.quiet().nothrow();
514
+ return;
515
+ }
516
+ }
517
+ await $`open -a Simulator`.quiet().nothrow();
518
+ }
148
519
  async function run(options) {
149
520
  const { simulator, output } = options;
150
521
  await build({ output });
@@ -155,15 +526,31 @@ async function run(options) {
155
526
  }
156
527
  const projectPath = join(output, xcodeproj);
157
528
  const appName = xcodeproj.replace(".xcodeproj", "");
529
+ const configPath = join(output, "craft.config.json");
530
+ if (!existsSync(configPath))
531
+ throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
532
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
158
533
  if (simulator) {
159
534
  console.log("\uD83D\uDCF1 Building and running on simulator...");
535
+ const device = await pickSimulator();
536
+ if (!device) {
537
+ throw new Error("No iOS simulator is available. Install a runtime with " + "`xcodebuild -downloadPlatform iOS`, then try again.");
538
+ }
539
+ console.log(` Device: ${device.name} (${device.runtime})`);
160
540
  try {
161
- await $`xcodebuild -project ${projectPath} -scheme ${appName} -destination 'platform=iOS Simulator,name=iPhone 15' build`;
162
- await $`xcrun simctl boot "iPhone 15"`.quiet().nothrow();
163
- await $`open -a Simulator`;
164
- console.log("\u2705 App deployed to simulator");
541
+ await bootSimulator(device);
542
+ const derivedData = join(output, "build");
543
+ const product = join(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
544
+ await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
545
+ if (!existsSync(product)) {
546
+ throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
547
+ }
548
+ await $`xcrun simctl install ${device.udid} ${product}`;
549
+ await showSimulator();
550
+ await $`xcrun simctl launch ${device.udid} ${config.bundleId}`;
551
+ console.log(`\u2705 ${config.appName} is running on ${device.name}`);
165
552
  } catch (error) {
166
- console.error("Build failed. Open in Xcode for details:", projectPath);
553
+ throw new Error(`iOS simulator run failed for ${projectPath}`, { cause: error });
167
554
  }
168
555
  } else {
169
556
  console.log("\uD83D\uDCF1 Opening Xcode for device deployment...");
@@ -177,10 +564,21 @@ async function run(options) {
177
564
  }
178
565
  }
179
566
  export {
567
+ syncWebAssets,
568
+ showSimulator,
180
569
  run,
570
+ renderWatchEntitlements,
571
+ renderUsageDescriptions,
572
+ renderUrlTypes,
573
+ renderPrivacyManifest,
574
+ renderOrientations,
575
+ renderEntitlements,
576
+ renderDeviceFamilies,
577
+ renderBackgroundModes,
578
+ pickSimulator,
579
+ orderSimulators,
181
580
  open,
182
581
  init,
183
- build
582
+ build,
583
+ bootSimulator
184
584
  };
185
-
186
- //# debugId=357E4FC7A8DD0D6264756E2164756E21
@@ -0,0 +1,14 @@
1
+ import ActivityKit
2
+
3
+ @available(iOS 16.1, *)
4
+ struct CraftActivityAttributes: ActivityAttributes {
5
+ struct ContentState: Codable, Hashable {
6
+ let status: String
7
+ let distanceMeters: Double
8
+ let durationSeconds: Double
9
+ let progress: Double
10
+ }
11
+
12
+ let activityId: String
13
+ let title: String
14
+ }