@craft-native/ios 0.0.70

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/index.js ADDED
@@ -0,0 +1,584 @@
1
+ // @bun
2
+ // src/index.ts
3
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
4
+ import { dirname, join, resolve } from "path";
5
+ var {$ } = globalThis.Bun;
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
+ }
282
+ async function init(options) {
283
+ const { name, bundleId, teamId, output } = options;
284
+ console.log(`
285
+ \u26A1 Initializing Craft iOS project: ${name}`);
286
+ console.log(` Output: ${output}
287
+ `);
288
+ const dirs = [output, join(output, "Sources"), join(output, "Shared"), join(output, "dist")];
289
+ if (options.config?.enableWatchApp)
290
+ dirs.push(join(output, "WatchApp"));
291
+ for (const dir of dirs) {
292
+ if (!existsSync(dir)) {
293
+ mkdirSync(dir, { recursive: true });
294
+ }
295
+ }
296
+ const finalBundleId = bundleId || `com.craft.${name.toLowerCase().replace(/[^a-z0-9]/g, "")}`;
297
+ const bundleIdPrefix = finalBundleId.split(".").slice(0, -1).join(".");
298
+ const config = {
299
+ ...DEFAULT_CONFIG,
300
+ appName: name,
301
+ bundleId: finalBundleId,
302
+ teamId: teamId || "",
303
+ ...options.config
304
+ };
305
+ if (config.enableBackgroundLocation)
306
+ config.enableGeolocation = true;
307
+ writeFileSync(join(output, "craft.config.json"), JSON.stringify(config, null, 2));
308
+ const swiftTemplate = readFileSync(join(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
309
+ const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
310
+ writeFileSync(join(output, "Sources", `${name}App.swift`), swiftSource);
311
+ const infoPlistTemplate = readFileSync(join(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
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/>` : "");
316
+ writeFileSync(join(output, "Info.plist"), infoPlist);
317
+ const projectYmlTemplate = readFileSync(join(TEMPLATES_DIR, "project.yml.template"), "utf-8");
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
+ `));
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
+ }
377
+ const placeholderHtml = `<!DOCTYPE html>
378
+ <html>
379
+ <head>
380
+ <meta charset="UTF-8">
381
+ <meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
382
+ <title>${name}</title>
383
+ <style>
384
+ * { margin: 0; padding: 0; box-sizing: border-box; }
385
+ body {
386
+ font-family: -apple-system, system-ui, sans-serif;
387
+ background: ${config.backgroundColor};
388
+ color: white;
389
+ min-height: 100vh;
390
+ display: flex;
391
+ justify-content: center;
392
+ align-items: center;
393
+ padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
394
+ }
395
+ .container { text-align: center; padding: 2rem; }
396
+ h1 { font-size: 2.5rem; margin-bottom: 1rem; }
397
+ p { opacity: 0.7; }
398
+ .ready { color: #4ade80; font-size: 0.9rem; margin-top: 2rem; }
399
+ </style>
400
+ </head>
401
+ <body>
402
+ <div class="container">
403
+ <h1>\u26A1 ${name}</h1>
404
+ <p>Built with Craft iOS</p>
405
+ <p class="ready" id="status">Waiting for Craft bridge...</p>
406
+ </div>
407
+ <script>
408
+ window.addEventListener('craftReady', (e) => {
409
+ document.getElementById('status').textContent = \`\u2713 Craft bridge ready (platform: \${e.detail.platform})\`;
410
+ console.log('Craft capabilities:', e.detail.capabilities);
411
+ });
412
+ </script>
413
+ </body>
414
+ </html>`;
415
+ writeFileSync(join(output, "dist", "index.html"), placeholderHtml);
416
+ console.log("\u2705 Project initialized");
417
+ console.log("");
418
+ console.log("Next steps:");
419
+ console.log(` 1. cd ${output}`);
420
+ console.log(" 2. Add your web content to dist/index.html");
421
+ console.log(" 3. Run: craft ios build");
422
+ console.log(" 4. Run: craft ios open");
423
+ console.log("");
424
+ }
425
+ async function build(options) {
426
+ const { htmlPath, devServer, output, generateProject = true } = options;
427
+ console.log(`
428
+ \uD83D\uDCE6 Building Craft iOS project...`);
429
+ const configPath = join(output, "craft.config.json");
430
+ if (!existsSync(configPath)) {
431
+ throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
432
+ }
433
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
434
+ if (devServer) {
435
+ config.devServerURL = devServer;
436
+ const origin = new URL(devServer).origin;
437
+ config.trustedOrigins = [...new Set([...config.trustedOrigins ?? [], origin])];
438
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
439
+ console.log(` Dev server: ${devServer}`);
440
+ }
441
+ if (htmlPath) {
442
+ syncWebAssets(htmlPath, output);
443
+ console.log(` Synced: ${htmlPath} \u2192 dist/`);
444
+ }
445
+ if (!generateProject)
446
+ return;
447
+ try {
448
+ const result = await $`which xcodegen`.quiet();
449
+ if (result.exitCode === 0) {
450
+ console.log(" Running xcodegen...");
451
+ await $`cd ${output} && xcodegen generate`.quiet();
452
+ console.log(`\u2705 Xcode project created: ${config.appName}.xcodeproj`);
453
+ } else {
454
+ throw new Error("xcodegen not found");
455
+ }
456
+ } catch (error) {
457
+ throw new Error("Unable to generate the Xcode project. Install xcodegen with `brew install xcodegen`.", {
458
+ cause: error
459
+ });
460
+ }
461
+ console.log("");
462
+ }
463
+ async function open(options) {
464
+ const { output } = options;
465
+ const files = readdirSync(output);
466
+ const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
467
+ if (!xcodeproj) {
468
+ throw new Error(`No Xcode project found in ${output}. Run 'craft ios build' first.`);
469
+ }
470
+ const projectPath = join(output, xcodeproj);
471
+ console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
472
+ await $`open ${projectPath}`;
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
+ }
519
+ async function run(options) {
520
+ const { simulator, output } = options;
521
+ await build({ output });
522
+ const files = readdirSync(output);
523
+ const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
524
+ if (!xcodeproj) {
525
+ throw new Error(`No Xcode project found in ${output}`);
526
+ }
527
+ const projectPath = join(output, xcodeproj);
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"));
533
+ if (simulator) {
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})`);
540
+ try {
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}`);
552
+ } catch (error) {
553
+ throw new Error(`iOS simulator run failed for ${projectPath}`, { cause: error });
554
+ }
555
+ } else {
556
+ console.log("\uD83D\uDCF1 Opening Xcode for device deployment...");
557
+ await $`open ${projectPath}`;
558
+ console.log("");
559
+ console.log("In Xcode:");
560
+ console.log(" 1. Select your Team in Signing & Capabilities");
561
+ console.log(" 2. Connect your iPhone");
562
+ console.log(" 3. Select your device");
563
+ console.log(" 4. Click Run (\u25B6\uFE0F)");
564
+ }
565
+ }
566
+ export {
567
+ syncWebAssets,
568
+ showSimulator,
569
+ run,
570
+ renderWatchEntitlements,
571
+ renderUsageDescriptions,
572
+ renderUrlTypes,
573
+ renderPrivacyManifest,
574
+ renderOrientations,
575
+ renderEntitlements,
576
+ renderDeviceFamilies,
577
+ renderBackgroundModes,
578
+ pickSimulator,
579
+ orderSimulators,
580
+ open,
581
+ init,
582
+ build,
583
+ bootSimulator
584
+ };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@craft-native/ios",
3
+ "version": "0.0.70",
4
+ "description": "iOS native app builder for Craft",
5
+ "type": "module",
6
+ "bin": {
7
+ "craft-ios": "./dist/cli.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "bun build.ts",
17
+ "dev": "bun --watch src/cli.ts"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "templates"
22
+ ],
23
+ "keywords": [
24
+ "craft",
25
+ "ios",
26
+ "mobile",
27
+ "native",
28
+ "swift",
29
+ "wkwebview"
30
+ ],
31
+ "author": "Chris Breuer <chris@stacksjs.org>",
32
+ "license": "MIT"
33
+ }
@@ -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
+ }