@craft-native/ios 0.0.70 → 0.0.72
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/LICENSE.md +21 -0
- package/package.json +11 -2
- package/dist/cli.js +0 -664
- package/dist/index.js +0 -584
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Stacks.js
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@craft-native/ios",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.72",
|
|
4
|
+
"homepage": "https://github.com/craft-native/craft/tree/main/packages/ios#readme",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/craft-native/craft.git",
|
|
8
|
+
"directory": "packages/ios"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/craft-native/craft/issues"
|
|
12
|
+
},
|
|
4
13
|
"description": "iOS native app builder for Craft",
|
|
5
14
|
"type": "module",
|
|
6
15
|
"bin": {
|
|
7
|
-
"craft-ios": "
|
|
16
|
+
"craft-ios": "dist/cli.js"
|
|
8
17
|
},
|
|
9
18
|
"exports": {
|
|
10
19
|
".": {
|
package/dist/cli.js
DELETED
|
@@ -1,664 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
// @bun
|
|
3
|
-
|
|
4
|
-
// src/cli.ts
|
|
5
|
-
import { parseArgs } from "util";
|
|
6
|
-
|
|
7
|
-
// src/index.ts
|
|
8
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
|
|
9
|
-
import { dirname, join, resolve } from "path";
|
|
10
|
-
var {$ } = globalThis.Bun;
|
|
11
|
-
var TEMPLATES_DIR = join(dirname(import.meta.dir), "templates");
|
|
12
|
-
var DEFAULT_CONFIG = {
|
|
13
|
-
version: "1.0.0",
|
|
14
|
-
buildNumber: "1",
|
|
15
|
-
darkMode: true,
|
|
16
|
-
backgroundColor: "#0b1712",
|
|
17
|
-
enableSpeechRecognition: false,
|
|
18
|
-
enableHaptics: false,
|
|
19
|
-
enableShare: false,
|
|
20
|
-
enableCamera: false,
|
|
21
|
-
enableBiometric: false,
|
|
22
|
-
enablePushNotifications: false,
|
|
23
|
-
enableSecureStorage: false,
|
|
24
|
-
enableGeolocation: false,
|
|
25
|
-
enableClipboard: false,
|
|
26
|
-
enableContacts: false,
|
|
27
|
-
enableCalendar: false,
|
|
28
|
-
enableLocalNotifications: false,
|
|
29
|
-
enableInAppPurchase: false,
|
|
30
|
-
enableKeepAwake: false,
|
|
31
|
-
enableOrientationLock: false,
|
|
32
|
-
enableDeepLinks: false,
|
|
33
|
-
enableQRScanner: false,
|
|
34
|
-
enableFilePicker: false,
|
|
35
|
-
enableFileDownload: false,
|
|
36
|
-
enableSocialAuth: false,
|
|
37
|
-
enableAudioRecording: false,
|
|
38
|
-
enableVideoRecording: false,
|
|
39
|
-
enableMotionSensors: false,
|
|
40
|
-
enableLocalDatabase: false,
|
|
41
|
-
enableBluetooth: false,
|
|
42
|
-
enableNFC: false,
|
|
43
|
-
enableHealthKit: false,
|
|
44
|
-
enableLiveActivities: false,
|
|
45
|
-
enableWatchApp: false,
|
|
46
|
-
enableBackgroundLocation: false,
|
|
47
|
-
enableBackgroundTasks: false,
|
|
48
|
-
enableScreenCapture: false,
|
|
49
|
-
enablePDFViewer: false,
|
|
50
|
-
enableAR: false,
|
|
51
|
-
enableMLKit: false,
|
|
52
|
-
iosVersion: "16.0",
|
|
53
|
-
watchosVersion: "9.0",
|
|
54
|
-
teamId: "",
|
|
55
|
-
trustedOrigins: [],
|
|
56
|
-
associatedDomains: [],
|
|
57
|
-
appGroups: [],
|
|
58
|
-
orientations: ["portrait"],
|
|
59
|
-
deviceFamilies: ["iphone", "ipad"]
|
|
60
|
-
};
|
|
61
|
-
function renderDeviceFamilies(config) {
|
|
62
|
-
const values = new Set(((config.deviceFamilies?.length) ? config.deviceFamilies : ["iphone", "ipad"]).map((family) => family === "ipad" ? "2" : "1"));
|
|
63
|
-
return [...values].join(",");
|
|
64
|
-
}
|
|
65
|
-
function xmlEscape(value) {
|
|
66
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll(/\u0027/g, "'");
|
|
67
|
-
}
|
|
68
|
-
function plistString(key, value) {
|
|
69
|
-
return ` <key>${key}</key>
|
|
70
|
-
<string>${xmlEscape(value)}</string>`;
|
|
71
|
-
}
|
|
72
|
-
function renderUsageDescriptions(config) {
|
|
73
|
-
const entries = [
|
|
74
|
-
[config.enableSpeechRecognition, "NSSpeechRecognitionUsageDescription", `${config.appName} uses speech recognition for voice input.`],
|
|
75
|
-
[config.enableSpeechRecognition || config.enableAudioRecording, "NSMicrophoneUsageDescription", `${config.appName} uses the microphone to record audio.`],
|
|
76
|
-
[config.enableCamera || config.enableVideoRecording || config.enableQRScanner || config.enableAR, "NSCameraUsageDescription", `${config.appName} uses the camera for photos, video, scanning, and augmented reality.`],
|
|
77
|
-
[config.enableCamera || config.enableVideoRecording, "NSPhotoLibraryUsageDescription", `${config.appName} lets you choose photos and videos from your library.`],
|
|
78
|
-
[config.enableGeolocation || config.enableBackgroundLocation, "NSLocationWhenInUseUsageDescription", `${config.appName} uses your location while you record an activity.`],
|
|
79
|
-
[config.enableBackgroundLocation, "NSLocationAlwaysAndWhenInUseUsageDescription", `${config.appName} continues recording your route when the screen is locked or the app is in the background.`],
|
|
80
|
-
[config.enableContacts, "NSContactsUsageDescription", `${config.appName} accesses contacts only when you choose a contact feature.`],
|
|
81
|
-
[config.enableCalendar, "NSCalendarsUsageDescription", `${config.appName} accesses your calendar only when you choose a calendar feature.`],
|
|
82
|
-
[config.enableBluetooth, "NSBluetoothAlwaysUsageDescription", `${config.appName} uses Bluetooth to connect to nearby devices.`],
|
|
83
|
-
[config.enableMotionSensors, "NSMotionUsageDescription", `${config.appName} uses motion data for activity features.`],
|
|
84
|
-
[config.enableNFC, "NFCReaderUsageDescription", `${config.appName} reads NFC tags when you start a scan.`],
|
|
85
|
-
[config.enableHealthKit, "NSHealthShareUsageDescription", `${config.appName} reads health data you choose to share.`],
|
|
86
|
-
[config.enableHealthKit, "NSHealthUpdateUsageDescription", `${config.appName} writes health data only with your permission.`],
|
|
87
|
-
[config.enableBiometric, "NSFaceIDUsageDescription", `${config.appName} uses Face ID to protect your account.`]
|
|
88
|
-
];
|
|
89
|
-
return entries.filter(([enabled]) => enabled).map(([, key, value]) => plistString(key, value)).join(`
|
|
90
|
-
`);
|
|
91
|
-
}
|
|
92
|
-
function renderOrientations(config) {
|
|
93
|
-
const names = {
|
|
94
|
-
portrait: "UIInterfaceOrientationPortrait",
|
|
95
|
-
"landscape-left": "UIInterfaceOrientationLandscapeLeft",
|
|
96
|
-
"landscape-right": "UIInterfaceOrientationLandscapeRight",
|
|
97
|
-
"portrait-upside-down": "UIInterfaceOrientationPortraitUpsideDown"
|
|
98
|
-
};
|
|
99
|
-
const values = config.orientations?.length ? config.orientations : ["portrait"];
|
|
100
|
-
return values.map((value) => ` <string>${names[value]}</string>`).join(`
|
|
101
|
-
`);
|
|
102
|
-
}
|
|
103
|
-
function renderUrlTypes(config) {
|
|
104
|
-
const schemes = [...new Set(config.urlSchemes?.map((value) => value.trim()).filter(Boolean) ?? [])];
|
|
105
|
-
if (!schemes.length)
|
|
106
|
-
return "";
|
|
107
|
-
return ` <key>CFBundleURLTypes</key>
|
|
108
|
-
<array>
|
|
109
|
-
<dict>
|
|
110
|
-
<key>CFBundleURLSchemes</key>
|
|
111
|
-
<array>
|
|
112
|
-
${schemes.map((value) => ` <string>${xmlEscape(value)}</string>`).join(`
|
|
113
|
-
`)}
|
|
114
|
-
</array>
|
|
115
|
-
</dict>
|
|
116
|
-
</array>`;
|
|
117
|
-
}
|
|
118
|
-
function plistArray(values, indent = 2) {
|
|
119
|
-
const padding = " ".repeat(indent);
|
|
120
|
-
return values.map((value) => `${padding}<string>${xmlEscape(value)}</string>`).join(`
|
|
121
|
-
`);
|
|
122
|
-
}
|
|
123
|
-
function renderBackgroundModes(config) {
|
|
124
|
-
const modes = new Set;
|
|
125
|
-
if (config.enableBackgroundLocation)
|
|
126
|
-
modes.add("location");
|
|
127
|
-
if (config.enableBackgroundTasks)
|
|
128
|
-
modes.add("processing");
|
|
129
|
-
if (config.enablePushNotifications)
|
|
130
|
-
modes.add("remote-notification");
|
|
131
|
-
if (!modes.size)
|
|
132
|
-
return "";
|
|
133
|
-
return ` <key>UIBackgroundModes</key>
|
|
134
|
-
<array>
|
|
135
|
-
${plistArray([...modes])}
|
|
136
|
-
</array>`;
|
|
137
|
-
}
|
|
138
|
-
function renderEntitlements(config) {
|
|
139
|
-
const entries = [];
|
|
140
|
-
if (config.associatedDomains?.length) {
|
|
141
|
-
entries.push(` <key>com.apple.developer.associated-domains</key>
|
|
142
|
-
<array>
|
|
143
|
-
${plistArray(config.associatedDomains)}
|
|
144
|
-
</array>`);
|
|
145
|
-
}
|
|
146
|
-
if (config.appGroups?.length) {
|
|
147
|
-
entries.push(` <key>com.apple.security.application-groups</key>
|
|
148
|
-
<array>
|
|
149
|
-
${plistArray(config.appGroups)}
|
|
150
|
-
</array>`);
|
|
151
|
-
}
|
|
152
|
-
if (config.enableHealthKit) {
|
|
153
|
-
entries.push(` <key>com.apple.developer.healthkit</key>
|
|
154
|
-
<true/>`);
|
|
155
|
-
}
|
|
156
|
-
if (config.enablePushNotifications) {
|
|
157
|
-
entries.push(` <key>aps-environment</key>
|
|
158
|
-
<string>development</string>`);
|
|
159
|
-
}
|
|
160
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
161
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
162
|
-
<plist version="1.0">
|
|
163
|
-
<dict>
|
|
164
|
-
${entries.join(`
|
|
165
|
-
`)}
|
|
166
|
-
</dict>
|
|
167
|
-
</plist>
|
|
168
|
-
`;
|
|
169
|
-
}
|
|
170
|
-
function renderWatchEntitlements(config) {
|
|
171
|
-
const appGroups = config.appGroups?.length ? ` <key>com.apple.security.application-groups</key>
|
|
172
|
-
<array>
|
|
173
|
-
${plistArray(config.appGroups)}
|
|
174
|
-
</array>
|
|
175
|
-
` : "";
|
|
176
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
177
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
178
|
-
<plist version="1.0">
|
|
179
|
-
<dict>
|
|
180
|
-
${appGroups}</dict>
|
|
181
|
-
</plist>
|
|
182
|
-
`;
|
|
183
|
-
}
|
|
184
|
-
function renderPrivacyManifest(config) {
|
|
185
|
-
const privacy = config.privacy ?? {};
|
|
186
|
-
const collected = privacy.collectedDataTypes ?? [];
|
|
187
|
-
const accessed = privacy.accessedApiTypes ?? [];
|
|
188
|
-
const collectedXml = collected.map((item) => ` <dict>
|
|
189
|
-
<key>NSPrivacyCollectedDataType</key>
|
|
190
|
-
<string>${xmlEscape(item.type)}</string>
|
|
191
|
-
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
192
|
-
<${item.linked ? "true" : "false"}/>
|
|
193
|
-
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
194
|
-
<${item.tracking ? "true" : "false"}/>
|
|
195
|
-
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
196
|
-
<array>
|
|
197
|
-
${plistArray(item.purposes, 4)}
|
|
198
|
-
</array>
|
|
199
|
-
</dict>`).join(`
|
|
200
|
-
`);
|
|
201
|
-
const accessedXml = accessed.map((item) => ` <dict>
|
|
202
|
-
<key>NSPrivacyAccessedAPIType</key>
|
|
203
|
-
<string>${xmlEscape(item.type)}</string>
|
|
204
|
-
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
205
|
-
<array>
|
|
206
|
-
${plistArray(item.reasons, 4)}
|
|
207
|
-
</array>
|
|
208
|
-
</dict>`).join(`
|
|
209
|
-
`);
|
|
210
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
211
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
212
|
-
<plist version="1.0">
|
|
213
|
-
<dict>
|
|
214
|
-
<key>NSPrivacyTracking</key>
|
|
215
|
-
<${privacy.tracking ? "true" : "false"}/>
|
|
216
|
-
<key>NSPrivacyTrackingDomains</key>
|
|
217
|
-
<array>
|
|
218
|
-
${plistArray(privacy.trackingDomains ?? [])}
|
|
219
|
-
</array>
|
|
220
|
-
<key>NSPrivacyCollectedDataTypes</key>
|
|
221
|
-
<array>
|
|
222
|
-
${collectedXml}
|
|
223
|
-
</array>
|
|
224
|
-
<key>NSPrivacyAccessedAPITypes</key>
|
|
225
|
-
<array>
|
|
226
|
-
${accessedXml}
|
|
227
|
-
</array>
|
|
228
|
-
</dict>
|
|
229
|
-
</plist>
|
|
230
|
-
`;
|
|
231
|
-
}
|
|
232
|
-
function renderAssetCatalog(output, config) {
|
|
233
|
-
const catalog = join(output, "Assets.xcassets");
|
|
234
|
-
const appIcon = join(catalog, "AppIcon.appiconset");
|
|
235
|
-
const launchBackground = join(catalog, "LaunchBackground.colorset");
|
|
236
|
-
mkdirSync(appIcon, { recursive: true });
|
|
237
|
-
mkdirSync(launchBackground, { recursive: true });
|
|
238
|
-
writeFileSync(join(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
|
|
239
|
-
`);
|
|
240
|
-
const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
|
|
241
|
-
if (config.appIconPath) {
|
|
242
|
-
if (!existsSync(config.appIconPath))
|
|
243
|
-
throw new Error(`App icon not found: ${config.appIconPath}`);
|
|
244
|
-
cpSync(config.appIconPath, join(appIcon, iconFilename));
|
|
245
|
-
}
|
|
246
|
-
writeFileSync(join(appIcon, "Contents.json"), `${JSON.stringify({
|
|
247
|
-
images: iconFilename ? [{
|
|
248
|
-
filename: iconFilename,
|
|
249
|
-
idiom: "universal",
|
|
250
|
-
platform: "ios",
|
|
251
|
-
size: "1024x1024"
|
|
252
|
-
}] : [],
|
|
253
|
-
info: { author: "xcode", version: 1 }
|
|
254
|
-
}, null, 2)}
|
|
255
|
-
`);
|
|
256
|
-
const color = config.backgroundColor?.replace(/^#/, "") || "000000";
|
|
257
|
-
const normalized = color.length === 3 ? [...color].map((value) => `${value}${value}`).join("") : color.padEnd(6, "0").slice(0, 6);
|
|
258
|
-
const components = [0, 2, 4].map((index) => (Number.parseInt(normalized.slice(index, index + 2), 16) / 255).toFixed(3));
|
|
259
|
-
writeFileSync(join(launchBackground, "Contents.json"), `${JSON.stringify({
|
|
260
|
-
colors: [{
|
|
261
|
-
color: {
|
|
262
|
-
"color-space": "srgb",
|
|
263
|
-
components: { alpha: "1.000", blue: components[2], green: components[1], red: components[0] }
|
|
264
|
-
},
|
|
265
|
-
idiom: "universal"
|
|
266
|
-
}],
|
|
267
|
-
info: { author: "xcode", version: 1 }
|
|
268
|
-
}, null, 2)}
|
|
269
|
-
`);
|
|
270
|
-
}
|
|
271
|
-
function syncWebAssets(source, output) {
|
|
272
|
-
const sourcePath = resolve(source);
|
|
273
|
-
if (!existsSync(sourcePath))
|
|
274
|
-
throw new Error(`Web asset path not found: ${source}`);
|
|
275
|
-
const distDir = join(output, "dist");
|
|
276
|
-
rmSync(distDir, { recursive: true, force: true });
|
|
277
|
-
mkdirSync(distDir, { recursive: true });
|
|
278
|
-
if (statSync(sourcePath).isDirectory()) {
|
|
279
|
-
cpSync(sourcePath, distDir, { recursive: true });
|
|
280
|
-
} else {
|
|
281
|
-
cpSync(sourcePath, join(distDir, "index.html"));
|
|
282
|
-
}
|
|
283
|
-
if (!existsSync(join(distDir, "index.html"))) {
|
|
284
|
-
throw new Error(`Web asset directory must contain index.html: ${source}`);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
async function init(options) {
|
|
288
|
-
const { name, bundleId, teamId, output } = options;
|
|
289
|
-
console.log(`
|
|
290
|
-
\u26A1 Initializing Craft iOS project: ${name}`);
|
|
291
|
-
console.log(` Output: ${output}
|
|
292
|
-
`);
|
|
293
|
-
const dirs = [output, join(output, "Sources"), join(output, "Shared"), join(output, "dist")];
|
|
294
|
-
if (options.config?.enableWatchApp)
|
|
295
|
-
dirs.push(join(output, "WatchApp"));
|
|
296
|
-
for (const dir of dirs) {
|
|
297
|
-
if (!existsSync(dir)) {
|
|
298
|
-
mkdirSync(dir, { recursive: true });
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
const finalBundleId = bundleId || `com.craft.${name.toLowerCase().replace(/[^a-z0-9]/g, "")}`;
|
|
302
|
-
const bundleIdPrefix = finalBundleId.split(".").slice(0, -1).join(".");
|
|
303
|
-
const config = {
|
|
304
|
-
...DEFAULT_CONFIG,
|
|
305
|
-
appName: name,
|
|
306
|
-
bundleId: finalBundleId,
|
|
307
|
-
teamId: teamId || "",
|
|
308
|
-
...options.config
|
|
309
|
-
};
|
|
310
|
-
if (config.enableBackgroundLocation)
|
|
311
|
-
config.enableGeolocation = true;
|
|
312
|
-
writeFileSync(join(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
313
|
-
const swiftTemplate = readFileSync(join(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
|
|
314
|
-
const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
315
|
-
writeFileSync(join(output, "Sources", `${name}App.swift`), swiftSource);
|
|
316
|
-
const infoPlistTemplate = readFileSync(join(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
|
|
317
|
-
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>
|
|
318
|
-
<true/>
|
|
319
|
-
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
|
|
320
|
-
<true/>` : "");
|
|
321
|
-
writeFileSync(join(output, "Info.plist"), infoPlist);
|
|
322
|
-
const projectYmlTemplate = readFileSync(join(TEMPLATES_DIR, "project.yml.template"), "utf-8");
|
|
323
|
-
const nativeDependencies = [];
|
|
324
|
-
if (config.enableLiveActivities)
|
|
325
|
-
nativeDependencies.push(` - target: ${name}LiveActivity`);
|
|
326
|
-
if (config.enableWatchApp)
|
|
327
|
-
nativeDependencies.push(` - target: ${name}Watch
|
|
328
|
-
embed: true`);
|
|
329
|
-
const nativeTargets = [];
|
|
330
|
-
if (config.enableLiveActivities) {
|
|
331
|
-
nativeTargets.push(` ${name}LiveActivity:
|
|
332
|
-
type: app-extension
|
|
333
|
-
platform: iOS
|
|
334
|
-
deploymentTarget: "16.2"
|
|
335
|
-
sources:
|
|
336
|
-
- WidgetExtension
|
|
337
|
-
- Shared
|
|
338
|
-
settings:
|
|
339
|
-
INFOPLIST_FILE: WidgetExtension/Info.plist
|
|
340
|
-
PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.liveactivity
|
|
341
|
-
SWIFT_VERSION: "5.0"
|
|
342
|
-
APPLICATION_EXTENSION_API_ONLY: YES
|
|
343
|
-
SKIP_INSTALL: YES`);
|
|
344
|
-
}
|
|
345
|
-
if (config.enableWatchApp) {
|
|
346
|
-
nativeTargets.push(` ${name}Watch:
|
|
347
|
-
type: application
|
|
348
|
-
platform: watchOS
|
|
349
|
-
deploymentTarget: "${config.watchosVersion || "9.0"}"
|
|
350
|
-
sources:
|
|
351
|
-
- WatchApp
|
|
352
|
-
settings:
|
|
353
|
-
INFOPLIST_FILE: WatchApp/Info.plist
|
|
354
|
-
CODE_SIGN_ENTITLEMENTS: WatchApp/Watch.entitlements
|
|
355
|
-
PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.watchkitapp
|
|
356
|
-
SWIFT_VERSION: "5.0"
|
|
357
|
-
SKIP_INSTALL: YES`);
|
|
358
|
-
}
|
|
359
|
-
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:
|
|
360
|
-
${nativeDependencies.join(`
|
|
361
|
-
`)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
|
|
362
|
-
`));
|
|
363
|
-
writeFileSync(join(output, "project.yml"), projectYml);
|
|
364
|
-
writeFileSync(join(output, "Craft.entitlements"), renderEntitlements(config));
|
|
365
|
-
writeFileSync(join(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
|
|
366
|
-
renderAssetCatalog(output, config);
|
|
367
|
-
cpSync(join(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join(output, "Shared", "CraftActivityAttributes.swift"));
|
|
368
|
-
if (config.enableLiveActivities) {
|
|
369
|
-
mkdirSync(join(output, "WidgetExtension"), { recursive: true });
|
|
370
|
-
const widgetSource = readFileSync(join(TEMPLATES_DIR, "CraftLiveActivityWidget.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
|
|
371
|
-
writeFileSync(join(output, "WidgetExtension", `${name}LiveActivity.swift`), widgetSource);
|
|
372
|
-
const widgetInfo = readFileSync(join(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
|
|
373
|
-
writeFileSync(join(output, "WidgetExtension", "Info.plist"), widgetInfo);
|
|
374
|
-
}
|
|
375
|
-
if (config.enableWatchApp) {
|
|
376
|
-
const watchSource = readFileSync(join(TEMPLATES_DIR, "CraftWatchApp.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
|
|
377
|
-
writeFileSync(join(output, "WatchApp", `${name}WatchApp.swift`), watchSource);
|
|
378
|
-
const watchInfo = readFileSync(join(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
379
|
-
writeFileSync(join(output, "WatchApp", "Info.plist"), watchInfo);
|
|
380
|
-
writeFileSync(join(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
381
|
-
}
|
|
382
|
-
const placeholderHtml = `<!DOCTYPE html>
|
|
383
|
-
<html>
|
|
384
|
-
<head>
|
|
385
|
-
<meta charset="UTF-8">
|
|
386
|
-
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
387
|
-
<title>${name}</title>
|
|
388
|
-
<style>
|
|
389
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
390
|
-
body {
|
|
391
|
-
font-family: -apple-system, system-ui, sans-serif;
|
|
392
|
-
background: ${config.backgroundColor};
|
|
393
|
-
color: white;
|
|
394
|
-
min-height: 100vh;
|
|
395
|
-
display: flex;
|
|
396
|
-
justify-content: center;
|
|
397
|
-
align-items: center;
|
|
398
|
-
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
|
399
|
-
}
|
|
400
|
-
.container { text-align: center; padding: 2rem; }
|
|
401
|
-
h1 { font-size: 2.5rem; margin-bottom: 1rem; }
|
|
402
|
-
p { opacity: 0.7; }
|
|
403
|
-
.ready { color: #4ade80; font-size: 0.9rem; margin-top: 2rem; }
|
|
404
|
-
</style>
|
|
405
|
-
</head>
|
|
406
|
-
<body>
|
|
407
|
-
<div class="container">
|
|
408
|
-
<h1>\u26A1 ${name}</h1>
|
|
409
|
-
<p>Built with Craft iOS</p>
|
|
410
|
-
<p class="ready" id="status">Waiting for Craft bridge...</p>
|
|
411
|
-
</div>
|
|
412
|
-
<script>
|
|
413
|
-
window.addEventListener('craftReady', (e) => {
|
|
414
|
-
document.getElementById('status').textContent = \`\u2713 Craft bridge ready (platform: \${e.detail.platform})\`;
|
|
415
|
-
console.log('Craft capabilities:', e.detail.capabilities);
|
|
416
|
-
});
|
|
417
|
-
</script>
|
|
418
|
-
</body>
|
|
419
|
-
</html>`;
|
|
420
|
-
writeFileSync(join(output, "dist", "index.html"), placeholderHtml);
|
|
421
|
-
console.log("\u2705 Project initialized");
|
|
422
|
-
console.log("");
|
|
423
|
-
console.log("Next steps:");
|
|
424
|
-
console.log(` 1. cd ${output}`);
|
|
425
|
-
console.log(" 2. Add your web content to dist/index.html");
|
|
426
|
-
console.log(" 3. Run: craft ios build");
|
|
427
|
-
console.log(" 4. Run: craft ios open");
|
|
428
|
-
console.log("");
|
|
429
|
-
}
|
|
430
|
-
async function build(options) {
|
|
431
|
-
const { htmlPath, devServer, output, generateProject = true } = options;
|
|
432
|
-
console.log(`
|
|
433
|
-
\uD83D\uDCE6 Building Craft iOS project...`);
|
|
434
|
-
const configPath = join(output, "craft.config.json");
|
|
435
|
-
if (!existsSync(configPath)) {
|
|
436
|
-
throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
|
|
437
|
-
}
|
|
438
|
-
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
439
|
-
if (devServer) {
|
|
440
|
-
config.devServerURL = devServer;
|
|
441
|
-
const origin = new URL(devServer).origin;
|
|
442
|
-
config.trustedOrigins = [...new Set([...config.trustedOrigins ?? [], origin])];
|
|
443
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
444
|
-
console.log(` Dev server: ${devServer}`);
|
|
445
|
-
}
|
|
446
|
-
if (htmlPath) {
|
|
447
|
-
syncWebAssets(htmlPath, output);
|
|
448
|
-
console.log(` Synced: ${htmlPath} \u2192 dist/`);
|
|
449
|
-
}
|
|
450
|
-
if (!generateProject)
|
|
451
|
-
return;
|
|
452
|
-
try {
|
|
453
|
-
const result = await $`which xcodegen`.quiet();
|
|
454
|
-
if (result.exitCode === 0) {
|
|
455
|
-
console.log(" Running xcodegen...");
|
|
456
|
-
await $`cd ${output} && xcodegen generate`.quiet();
|
|
457
|
-
console.log(`\u2705 Xcode project created: ${config.appName}.xcodeproj`);
|
|
458
|
-
} else {
|
|
459
|
-
throw new Error("xcodegen not found");
|
|
460
|
-
}
|
|
461
|
-
} catch (error) {
|
|
462
|
-
throw new Error("Unable to generate the Xcode project. Install xcodegen with `brew install xcodegen`.", {
|
|
463
|
-
cause: error
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
console.log("");
|
|
467
|
-
}
|
|
468
|
-
async function open(options) {
|
|
469
|
-
const { output } = options;
|
|
470
|
-
const files = readdirSync(output);
|
|
471
|
-
const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
|
|
472
|
-
if (!xcodeproj) {
|
|
473
|
-
throw new Error(`No Xcode project found in ${output}. Run 'craft ios build' first.`);
|
|
474
|
-
}
|
|
475
|
-
const projectPath = join(output, xcodeproj);
|
|
476
|
-
console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
|
|
477
|
-
await $`open ${projectPath}`;
|
|
478
|
-
}
|
|
479
|
-
function orderSimulators(devices) {
|
|
480
|
-
return [...devices].sort((left, right) => {
|
|
481
|
-
const booted = Number(right.state === "Booted") - Number(left.state === "Booted");
|
|
482
|
-
if (booted !== 0)
|
|
483
|
-
return booted;
|
|
484
|
-
const phone = Number(right.name.startsWith("iPhone")) - Number(left.name.startsWith("iPhone"));
|
|
485
|
-
if (phone !== 0)
|
|
486
|
-
return phone;
|
|
487
|
-
return right.runtime.localeCompare(left.runtime);
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
async function pickSimulator() {
|
|
491
|
-
const listed = await $`xcrun simctl list devices available --json`.quiet().nothrow();
|
|
492
|
-
if (listed.exitCode !== 0)
|
|
493
|
-
return null;
|
|
494
|
-
let parsed;
|
|
495
|
-
try {
|
|
496
|
-
parsed = JSON.parse(listed.stdout.toString());
|
|
497
|
-
} catch {
|
|
498
|
-
return null;
|
|
499
|
-
}
|
|
500
|
-
const devices = [];
|
|
501
|
-
for (const [runtime, list] of Object.entries(parsed.devices ?? {})) {
|
|
502
|
-
for (const device of list)
|
|
503
|
-
devices.push({ ...device, runtime: runtime.split(".").pop() ?? runtime });
|
|
504
|
-
}
|
|
505
|
-
return orderSimulators(devices)[0] ?? null;
|
|
506
|
-
}
|
|
507
|
-
async function bootSimulator(device) {
|
|
508
|
-
if (device.state !== "Booted")
|
|
509
|
-
await $`xcrun simctl boot ${device.udid}`.quiet().nothrow();
|
|
510
|
-
await $`xcrun simctl bootstatus ${device.udid}`.quiet().nothrow();
|
|
511
|
-
}
|
|
512
|
-
async function showSimulator() {
|
|
513
|
-
const selected = await $`xcode-select -p`.quiet().nothrow();
|
|
514
|
-
const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
|
|
515
|
-
if (developer) {
|
|
516
|
-
const app = join(developer, "Applications", "Simulator.app");
|
|
517
|
-
if (existsSync(app)) {
|
|
518
|
-
await $`open ${app}`.quiet().nothrow();
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
await $`open -a Simulator`.quiet().nothrow();
|
|
523
|
-
}
|
|
524
|
-
async function run(options) {
|
|
525
|
-
const { simulator, output } = options;
|
|
526
|
-
await build({ output });
|
|
527
|
-
const files = readdirSync(output);
|
|
528
|
-
const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
|
|
529
|
-
if (!xcodeproj) {
|
|
530
|
-
throw new Error(`No Xcode project found in ${output}`);
|
|
531
|
-
}
|
|
532
|
-
const projectPath = join(output, xcodeproj);
|
|
533
|
-
const appName = xcodeproj.replace(".xcodeproj", "");
|
|
534
|
-
const configPath = join(output, "craft.config.json");
|
|
535
|
-
if (!existsSync(configPath))
|
|
536
|
-
throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
|
|
537
|
-
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
538
|
-
if (simulator) {
|
|
539
|
-
console.log("\uD83D\uDCF1 Building and running on simulator...");
|
|
540
|
-
const device = await pickSimulator();
|
|
541
|
-
if (!device) {
|
|
542
|
-
throw new Error("No iOS simulator is available. Install a runtime with " + "`xcodebuild -downloadPlatform iOS`, then try again.");
|
|
543
|
-
}
|
|
544
|
-
console.log(` Device: ${device.name} (${device.runtime})`);
|
|
545
|
-
try {
|
|
546
|
-
await bootSimulator(device);
|
|
547
|
-
const derivedData = join(output, "build");
|
|
548
|
-
const product = join(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
|
|
549
|
-
await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
|
|
550
|
-
if (!existsSync(product)) {
|
|
551
|
-
throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
|
|
552
|
-
}
|
|
553
|
-
await $`xcrun simctl install ${device.udid} ${product}`;
|
|
554
|
-
await showSimulator();
|
|
555
|
-
await $`xcrun simctl launch ${device.udid} ${config.bundleId}`;
|
|
556
|
-
console.log(`\u2705 ${config.appName} is running on ${device.name}`);
|
|
557
|
-
} catch (error) {
|
|
558
|
-
throw new Error(`iOS simulator run failed for ${projectPath}`, { cause: error });
|
|
559
|
-
}
|
|
560
|
-
} else {
|
|
561
|
-
console.log("\uD83D\uDCF1 Opening Xcode for device deployment...");
|
|
562
|
-
await $`open ${projectPath}`;
|
|
563
|
-
console.log("");
|
|
564
|
-
console.log("In Xcode:");
|
|
565
|
-
console.log(" 1. Select your Team in Signing & Capabilities");
|
|
566
|
-
console.log(" 2. Connect your iPhone");
|
|
567
|
-
console.log(" 3. Select your device");
|
|
568
|
-
console.log(" 4. Click Run (\u25B6\uFE0F)");
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// src/cli.ts
|
|
573
|
-
var { values, positionals } = parseArgs({
|
|
574
|
-
args: Bun.argv.slice(2),
|
|
575
|
-
options: {
|
|
576
|
-
help: { type: "boolean", short: "h" },
|
|
577
|
-
version: { type: "boolean", short: "v" },
|
|
578
|
-
name: { type: "string", short: "n" },
|
|
579
|
-
"bundle-id": { type: "string", short: "b" },
|
|
580
|
-
"team-id": { type: "string", short: "t" },
|
|
581
|
-
"html-path": { type: "string" },
|
|
582
|
-
"dev-server": { type: "string", short: "d" },
|
|
583
|
-
output: { type: "string", short: "o" },
|
|
584
|
-
simulator: { type: "boolean", short: "s" }
|
|
585
|
-
},
|
|
586
|
-
allowPositionals: true
|
|
587
|
-
});
|
|
588
|
-
var command = positionals[0];
|
|
589
|
-
if (values.help || !command) {
|
|
590
|
-
console.log(`
|
|
591
|
-
Craft iOS - Build native iOS apps with web technologies
|
|
592
|
-
|
|
593
|
-
Usage:
|
|
594
|
-
craft-ios <command> [options]
|
|
595
|
-
|
|
596
|
-
Commands:
|
|
597
|
-
init <name> Initialize a new iOS project
|
|
598
|
-
build Build web assets and generate Xcode project
|
|
599
|
-
open Open Xcode project
|
|
600
|
-
run Build and run on simulator
|
|
601
|
-
|
|
602
|
-
Options:
|
|
603
|
-
-n, --name <name> App name
|
|
604
|
-
-b, --bundle-id <id> Bundle identifier (e.g., com.example.app)
|
|
605
|
-
-t, --team-id <id> Apple Developer Team ID
|
|
606
|
-
--html-path <path> Path to HTML file or directory
|
|
607
|
-
-d, --dev-server <url> Development server URL
|
|
608
|
-
-o, --output <dir> Output directory (default: ./ios)
|
|
609
|
-
-s, --simulator Run on simulator instead of device
|
|
610
|
-
-h, --help Show help
|
|
611
|
-
-v, --version Show version
|
|
612
|
-
|
|
613
|
-
Examples:
|
|
614
|
-
craft-ios init MyApp
|
|
615
|
-
craft-ios build --html-path ./dist/index.html
|
|
616
|
-
craft-ios build --dev-server http://localhost:3456
|
|
617
|
-
craft-ios run --simulator
|
|
618
|
-
`);
|
|
619
|
-
process.exit(0);
|
|
620
|
-
}
|
|
621
|
-
if (values.version) {
|
|
622
|
-
console.log("craft-ios v0.1.0");
|
|
623
|
-
process.exit(0);
|
|
624
|
-
}
|
|
625
|
-
async function main() {
|
|
626
|
-
try {
|
|
627
|
-
switch (command) {
|
|
628
|
-
case "init":
|
|
629
|
-
await init({
|
|
630
|
-
name: positionals[1] || values.name || "CraftApp",
|
|
631
|
-
bundleId: values["bundle-id"],
|
|
632
|
-
teamId: values["team-id"],
|
|
633
|
-
output: values.output || "./ios"
|
|
634
|
-
});
|
|
635
|
-
break;
|
|
636
|
-
case "build":
|
|
637
|
-
await build({
|
|
638
|
-
htmlPath: values["html-path"],
|
|
639
|
-
devServer: values["dev-server"],
|
|
640
|
-
output: values.output || "./ios"
|
|
641
|
-
});
|
|
642
|
-
break;
|
|
643
|
-
case "open":
|
|
644
|
-
await open({
|
|
645
|
-
output: values.output || "./ios"
|
|
646
|
-
});
|
|
647
|
-
break;
|
|
648
|
-
case "run":
|
|
649
|
-
await run({
|
|
650
|
-
simulator: values.simulator || false,
|
|
651
|
-
output: values.output || "./ios"
|
|
652
|
-
});
|
|
653
|
-
break;
|
|
654
|
-
default:
|
|
655
|
-
console.error(`Unknown command: ${command}`);
|
|
656
|
-
console.log("Run craft-ios --help for usage");
|
|
657
|
-
process.exit(1);
|
|
658
|
-
}
|
|
659
|
-
} catch (error) {
|
|
660
|
-
console.error("Error:", error instanceof Error ? error.message : error);
|
|
661
|
-
process.exit(1);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
main();
|
package/dist/index.js
DELETED
|
@@ -1,584 +0,0 @@
|
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll(/\u0027/g, "'");
|
|
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
|
-
};
|