craft-native 0.0.105 → 0.0.107
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/android/src/index.d.ts +2 -0
- package/dist/android/src/index.js +33 -6
- package/dist/android/templates/MainActivityNative.kt.template +390 -0
- package/dist/android/templates/build.gradle.kts.app.template +1 -0
- package/dist/api/window.d.ts +6 -11
- package/dist/cli.js +79 -1480
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +4 -0
- package/dist/ios/src/index.js +32 -3
- package/dist/ios/templates/CraftApp.swift +20 -92
- package/dist/ios/templates/CraftNativeActions.swift +93 -0
- package/dist/ios/templates/CraftNativeScreen.swift +381 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1139,1416 +1139,6 @@ var init_package = __esm(() => {
|
|
|
1139
1139
|
MEBIBYTE = 1024 * 1024;
|
|
1140
1140
|
});
|
|
1141
1141
|
|
|
1142
|
-
// dist/ios/src/index.js
|
|
1143
|
-
var exports_src = {};
|
|
1144
|
-
__export(exports_src, {
|
|
1145
|
-
syncWebAssets: () => syncWebAssets,
|
|
1146
|
-
showSimulator: () => showSimulator,
|
|
1147
|
-
run: () => run,
|
|
1148
|
-
resolveRuntimeDir: () => resolveRuntimeDir,
|
|
1149
|
-
resolveAppearance: () => resolveAppearance,
|
|
1150
|
-
renderWatchEntitlements: () => renderWatchEntitlements,
|
|
1151
|
-
renderUsageDescriptions: () => renderUsageDescriptions,
|
|
1152
|
-
renderUrlTypes: () => renderUrlTypes,
|
|
1153
|
-
renderRuntimeSettings: () => renderRuntimeSettings,
|
|
1154
|
-
renderPrivacyManifest: () => renderPrivacyManifest,
|
|
1155
|
-
renderOrientations: () => renderOrientations,
|
|
1156
|
-
renderEntitlements: () => renderEntitlements,
|
|
1157
|
-
renderDeviceFamilies: () => renderDeviceFamilies,
|
|
1158
|
-
renderBackgroundModes: () => renderBackgroundModes,
|
|
1159
|
-
renderAppearance: () => renderAppearance,
|
|
1160
|
-
productName: () => productName,
|
|
1161
|
-
pickSimulator: () => pickSimulator,
|
|
1162
|
-
orderSimulators: () => orderSimulators,
|
|
1163
|
-
open: () => open,
|
|
1164
|
-
installRuntime: () => installRuntime,
|
|
1165
|
-
init: () => init,
|
|
1166
|
-
build: () => build,
|
|
1167
|
-
bootSimulator: () => bootSimulator
|
|
1168
|
-
});
|
|
1169
|
-
import { cpSync as cpSync2, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1170
|
-
import { dirname, join as join2, resolve } from "path";
|
|
1171
|
-
function renderDeviceFamilies(config) {
|
|
1172
|
-
const values = new Set(((config.deviceFamilies?.length) ? config.deviceFamilies : ["iphone", "ipad"]).map((family) => family === "ipad" ? "2" : "1"));
|
|
1173
|
-
return [...values].join(",");
|
|
1174
|
-
}
|
|
1175
|
-
function xmlEscape(value) {
|
|
1176
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll(/\u0027/g, "'");
|
|
1177
|
-
}
|
|
1178
|
-
function plistString(key, value) {
|
|
1179
|
-
return ` <key>${key}</key>
|
|
1180
|
-
<string>${xmlEscape(value)}</string>`;
|
|
1181
|
-
}
|
|
1182
|
-
function resolveAppearance(config) {
|
|
1183
|
-
if (config.appearance)
|
|
1184
|
-
return config.appearance;
|
|
1185
|
-
return config.darkMode === false ? "light" : "dark";
|
|
1186
|
-
}
|
|
1187
|
-
function renderAppearance(config) {
|
|
1188
|
-
const appearance = resolveAppearance(config);
|
|
1189
|
-
if (appearance === "system")
|
|
1190
|
-
return { interfaceStyle: "Automatic", statusBarStyle: "UIStatusBarStyleDefault" };
|
|
1191
|
-
if (appearance === "light")
|
|
1192
|
-
return { interfaceStyle: "Light", statusBarStyle: "UIStatusBarStyleDarkContent" };
|
|
1193
|
-
return { interfaceStyle: "Dark", statusBarStyle: "UIStatusBarStyleLightContent" };
|
|
1194
|
-
}
|
|
1195
|
-
function renderUsageDescriptions(config) {
|
|
1196
|
-
const entries = [
|
|
1197
|
-
[config.enableSpeechRecognition, "NSSpeechRecognitionUsageDescription", `${config.appName} uses speech recognition for voice input.`],
|
|
1198
|
-
[config.enableSpeechRecognition || config.enableAudioRecording, "NSMicrophoneUsageDescription", `${config.appName} uses the microphone to record audio.`],
|
|
1199
|
-
[config.enableCamera || config.enableVideoRecording || config.enableQRScanner || config.enableAR, "NSCameraUsageDescription", `${config.appName} uses the camera for photos, video, scanning, and augmented reality.`],
|
|
1200
|
-
[config.enableCamera || config.enableVideoRecording, "NSPhotoLibraryUsageDescription", `${config.appName} lets you choose photos and videos from your library.`],
|
|
1201
|
-
[config.enableGeolocation || config.enableBackgroundLocation, "NSLocationWhenInUseUsageDescription", `${config.appName} uses your location while you record an activity.`],
|
|
1202
|
-
[config.enableBackgroundLocation, "NSLocationAlwaysAndWhenInUseUsageDescription", `${config.appName} continues recording your route when the screen is locked or the app is in the background.`],
|
|
1203
|
-
[config.enableContacts, "NSContactsUsageDescription", `${config.appName} accesses contacts only when you choose a contact feature.`],
|
|
1204
|
-
[config.enableCalendar, "NSCalendarsUsageDescription", `${config.appName} accesses your calendar only when you choose a calendar feature.`],
|
|
1205
|
-
[config.enableBluetooth, "NSBluetoothAlwaysUsageDescription", `${config.appName} uses Bluetooth to connect to nearby devices.`],
|
|
1206
|
-
[config.enableMotionSensors, "NSMotionUsageDescription", `${config.appName} uses motion data for activity features.`],
|
|
1207
|
-
[config.enableNFC, "NFCReaderUsageDescription", `${config.appName} reads NFC tags when you start a scan.`],
|
|
1208
|
-
[config.enableHealthKit, "NSHealthShareUsageDescription", `${config.appName} reads health data you choose to share.`],
|
|
1209
|
-
[config.enableHealthKit, "NSHealthUpdateUsageDescription", `${config.appName} writes health data only with your permission.`],
|
|
1210
|
-
[config.enableBiometric, "NSFaceIDUsageDescription", `${config.appName} uses Face ID to protect your account.`]
|
|
1211
|
-
];
|
|
1212
|
-
return entries.filter(([enabled2]) => enabled2).map(([, key, value]) => plistString(key, value)).join(`
|
|
1213
|
-
`);
|
|
1214
|
-
}
|
|
1215
|
-
function renderOrientations(config) {
|
|
1216
|
-
const names = {
|
|
1217
|
-
portrait: "UIInterfaceOrientationPortrait",
|
|
1218
|
-
"landscape-left": "UIInterfaceOrientationLandscapeLeft",
|
|
1219
|
-
"landscape-right": "UIInterfaceOrientationLandscapeRight",
|
|
1220
|
-
"portrait-upside-down": "UIInterfaceOrientationPortraitUpsideDown"
|
|
1221
|
-
};
|
|
1222
|
-
const values = config.orientations?.length ? config.orientations : ["portrait"];
|
|
1223
|
-
return values.map((value) => ` <string>${names[value]}</string>`).join(`
|
|
1224
|
-
`);
|
|
1225
|
-
}
|
|
1226
|
-
function renderUrlTypes(config) {
|
|
1227
|
-
const schemes = [...new Set(config.urlSchemes?.map((value) => value.trim()).filter(Boolean) ?? [])];
|
|
1228
|
-
if (!schemes.length)
|
|
1229
|
-
return "";
|
|
1230
|
-
return ` <key>CFBundleURLTypes</key>
|
|
1231
|
-
<array>
|
|
1232
|
-
<dict>
|
|
1233
|
-
<key>CFBundleURLSchemes</key>
|
|
1234
|
-
<array>
|
|
1235
|
-
${schemes.map((value) => ` <string>${xmlEscape(value)}</string>`).join(`
|
|
1236
|
-
`)}
|
|
1237
|
-
</array>
|
|
1238
|
-
</dict>
|
|
1239
|
-
</array>`;
|
|
1240
|
-
}
|
|
1241
|
-
function plistArray(values, indent = 2) {
|
|
1242
|
-
const padding = " ".repeat(indent);
|
|
1243
|
-
return values.map((value) => `${padding}<string>${xmlEscape(value)}</string>`).join(`
|
|
1244
|
-
`);
|
|
1245
|
-
}
|
|
1246
|
-
function renderBackgroundModes(config) {
|
|
1247
|
-
const modes = new Set;
|
|
1248
|
-
if (config.enableBackgroundLocation)
|
|
1249
|
-
modes.add("location");
|
|
1250
|
-
if (config.enableBackgroundTasks)
|
|
1251
|
-
modes.add("processing");
|
|
1252
|
-
if (config.enablePushNotifications)
|
|
1253
|
-
modes.add("remote-notification");
|
|
1254
|
-
if (!modes.size)
|
|
1255
|
-
return "";
|
|
1256
|
-
return ` <key>UIBackgroundModes</key>
|
|
1257
|
-
<array>
|
|
1258
|
-
${plistArray([...modes])}
|
|
1259
|
-
</array>`;
|
|
1260
|
-
}
|
|
1261
|
-
function renderEntitlements(config) {
|
|
1262
|
-
const entries = [];
|
|
1263
|
-
if (config.associatedDomains?.length) {
|
|
1264
|
-
entries.push(` <key>com.apple.developer.associated-domains</key>
|
|
1265
|
-
<array>
|
|
1266
|
-
${plistArray(config.associatedDomains)}
|
|
1267
|
-
</array>`);
|
|
1268
|
-
}
|
|
1269
|
-
if (config.appGroups?.length) {
|
|
1270
|
-
entries.push(` <key>com.apple.security.application-groups</key>
|
|
1271
|
-
<array>
|
|
1272
|
-
${plistArray(config.appGroups)}
|
|
1273
|
-
</array>`);
|
|
1274
|
-
}
|
|
1275
|
-
if (config.enableHealthKit) {
|
|
1276
|
-
entries.push(` <key>com.apple.developer.healthkit</key>
|
|
1277
|
-
<true/>`);
|
|
1278
|
-
}
|
|
1279
|
-
if (config.enablePushNotifications) {
|
|
1280
|
-
entries.push(` <key>aps-environment</key>
|
|
1281
|
-
<string>$(CRAFT_APNS_ENVIRONMENT)</string>`);
|
|
1282
|
-
}
|
|
1283
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1284
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1285
|
-
<plist version="1.0">
|
|
1286
|
-
<dict>
|
|
1287
|
-
${entries.join(`
|
|
1288
|
-
`)}
|
|
1289
|
-
</dict>
|
|
1290
|
-
</plist>
|
|
1291
|
-
`;
|
|
1292
|
-
}
|
|
1293
|
-
function renderWatchEntitlements(config) {
|
|
1294
|
-
const appGroups = config.appGroups?.length ? ` <key>com.apple.security.application-groups</key>
|
|
1295
|
-
<array>
|
|
1296
|
-
${plistArray(config.appGroups)}
|
|
1297
|
-
</array>
|
|
1298
|
-
` : "";
|
|
1299
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1300
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1301
|
-
<plist version="1.0">
|
|
1302
|
-
<dict>
|
|
1303
|
-
${appGroups}</dict>
|
|
1304
|
-
</plist>
|
|
1305
|
-
`;
|
|
1306
|
-
}
|
|
1307
|
-
function renderRuntimeSettings() {
|
|
1308
|
-
return [
|
|
1309
|
-
' LIBRARY_SEARCH_PATHS[sdk=iphoneos*]: "$(PROJECT_DIR)/Runtime/device"',
|
|
1310
|
-
' LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]: "$(PROJECT_DIR)/Runtime/simulator"',
|
|
1311
|
-
' OTHER_LDFLAGS: "-lcraft-ios -Wl,-u,_craft_ios_handle_action -Wl,-u,_craft_ios_set_webview ' + '-Wl,-u,_craft_ios_deliver_result -Wl,-u,_craft_ios_deliver_error"'
|
|
1312
|
-
].join(`
|
|
1313
|
-
`);
|
|
1314
|
-
}
|
|
1315
|
-
function renderPrivacyManifest(config) {
|
|
1316
|
-
const privacy = config.privacy ?? {};
|
|
1317
|
-
const collected = privacy.collectedDataTypes ?? [];
|
|
1318
|
-
const accessed = privacy.accessedApiTypes ?? [];
|
|
1319
|
-
const collectedXml = collected.map((item) => ` <dict>
|
|
1320
|
-
<key>NSPrivacyCollectedDataType</key>
|
|
1321
|
-
<string>${xmlEscape(item.type)}</string>
|
|
1322
|
-
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
1323
|
-
<${item.linked ? "true" : "false"}/>
|
|
1324
|
-
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
1325
|
-
<${item.tracking ? "true" : "false"}/>
|
|
1326
|
-
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
1327
|
-
<array>
|
|
1328
|
-
${plistArray(item.purposes, 4)}
|
|
1329
|
-
</array>
|
|
1330
|
-
</dict>`).join(`
|
|
1331
|
-
`);
|
|
1332
|
-
const accessedXml = accessed.map((item) => ` <dict>
|
|
1333
|
-
<key>NSPrivacyAccessedAPIType</key>
|
|
1334
|
-
<string>${xmlEscape(item.type)}</string>
|
|
1335
|
-
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
1336
|
-
<array>
|
|
1337
|
-
${plistArray(item.reasons, 4)}
|
|
1338
|
-
</array>
|
|
1339
|
-
</dict>`).join(`
|
|
1340
|
-
`);
|
|
1341
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1342
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1343
|
-
<plist version="1.0">
|
|
1344
|
-
<dict>
|
|
1345
|
-
<key>NSPrivacyTracking</key>
|
|
1346
|
-
<${privacy.tracking ? "true" : "false"}/>
|
|
1347
|
-
<key>NSPrivacyTrackingDomains</key>
|
|
1348
|
-
<array>
|
|
1349
|
-
${plistArray(privacy.trackingDomains ?? [])}
|
|
1350
|
-
</array>
|
|
1351
|
-
<key>NSPrivacyCollectedDataTypes</key>
|
|
1352
|
-
<array>
|
|
1353
|
-
${collectedXml}
|
|
1354
|
-
</array>
|
|
1355
|
-
<key>NSPrivacyAccessedAPITypes</key>
|
|
1356
|
-
<array>
|
|
1357
|
-
${accessedXml}
|
|
1358
|
-
</array>
|
|
1359
|
-
</dict>
|
|
1360
|
-
</plist>
|
|
1361
|
-
`;
|
|
1362
|
-
}
|
|
1363
|
-
function renderAssetCatalog(output, config) {
|
|
1364
|
-
const catalog = join2(output, "Assets.xcassets");
|
|
1365
|
-
const appIcon = join2(catalog, "AppIcon.appiconset");
|
|
1366
|
-
const launchBackground = join2(catalog, "LaunchBackground.colorset");
|
|
1367
|
-
mkdirSync2(appIcon, { recursive: true });
|
|
1368
|
-
mkdirSync2(launchBackground, { recursive: true });
|
|
1369
|
-
writeFileSync3(join2(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
|
|
1370
|
-
`);
|
|
1371
|
-
const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
|
|
1372
|
-
if (config.appIconPath) {
|
|
1373
|
-
if (!existsSync4(config.appIconPath))
|
|
1374
|
-
throw new Error(`App icon not found: ${config.appIconPath}`);
|
|
1375
|
-
cpSync2(config.appIconPath, join2(appIcon, iconFilename));
|
|
1376
|
-
}
|
|
1377
|
-
writeFileSync3(join2(appIcon, "Contents.json"), `${JSON.stringify({
|
|
1378
|
-
images: iconFilename ? [{
|
|
1379
|
-
filename: iconFilename,
|
|
1380
|
-
idiom: "universal",
|
|
1381
|
-
platform: "ios",
|
|
1382
|
-
size: "1024x1024"
|
|
1383
|
-
}] : [],
|
|
1384
|
-
info: { author: "xcode", version: 1 }
|
|
1385
|
-
}, null, 2)}
|
|
1386
|
-
`);
|
|
1387
|
-
const colorEntry = (hex) => {
|
|
1388
|
-
const color = hex?.replace(/^#/, "") || "000000";
|
|
1389
|
-
const normalized = color.length === 3 ? [...color].map((value) => `${value}${value}`).join("") : color.padEnd(6, "0").slice(0, 6);
|
|
1390
|
-
const components = [0, 2, 4].map((index) => (Number.parseInt(normalized.slice(index, index + 2), 16) / 255).toFixed(3));
|
|
1391
|
-
return {
|
|
1392
|
-
"color-space": "srgb",
|
|
1393
|
-
components: { alpha: "1.000", blue: components[2], green: components[1], red: components[0] }
|
|
1394
|
-
};
|
|
1395
|
-
};
|
|
1396
|
-
const colors2 = [{ color: colorEntry(config.backgroundColor), idiom: "universal" }];
|
|
1397
|
-
if (config.backgroundColorDark) {
|
|
1398
|
-
colors2.push({
|
|
1399
|
-
appearances: [{ appearance: "luminosity", value: "dark" }],
|
|
1400
|
-
color: colorEntry(config.backgroundColorDark),
|
|
1401
|
-
idiom: "universal"
|
|
1402
|
-
});
|
|
1403
|
-
}
|
|
1404
|
-
writeFileSync3(join2(launchBackground, "Contents.json"), `${JSON.stringify({
|
|
1405
|
-
colors: colors2,
|
|
1406
|
-
info: { author: "xcode", version: 1 }
|
|
1407
|
-
}, null, 2)}
|
|
1408
|
-
`);
|
|
1409
|
-
}
|
|
1410
|
-
function syncWebAssets(source, output) {
|
|
1411
|
-
const sourcePath = resolve(source);
|
|
1412
|
-
if (!existsSync4(sourcePath))
|
|
1413
|
-
throw new Error(`Web asset path not found: ${source}`);
|
|
1414
|
-
const distDir = join2(output, "dist");
|
|
1415
|
-
rmSync2(distDir, { recursive: true, force: true });
|
|
1416
|
-
mkdirSync2(distDir, { recursive: true });
|
|
1417
|
-
if (statSync(sourcePath).isDirectory()) {
|
|
1418
|
-
cpSync2(sourcePath, distDir, { recursive: true });
|
|
1419
|
-
} else {
|
|
1420
|
-
cpSync2(sourcePath, join2(distDir, "index.html"));
|
|
1421
|
-
}
|
|
1422
|
-
if (!existsSync4(join2(distDir, "index.html"))) {
|
|
1423
|
-
throw new Error(`Web asset directory must contain index.html: ${source}`);
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
function resolveRuntimeDir(override) {
|
|
1427
|
-
if (override === null)
|
|
1428
|
-
return null;
|
|
1429
|
-
const dir = override ?? process.env.CRAFT_IOS_RUNTIME;
|
|
1430
|
-
if (!dir)
|
|
1431
|
-
return null;
|
|
1432
|
-
if (!existsSync4(dir)) {
|
|
1433
|
-
const source = override === undefined ? "CRAFT_IOS_RUNTIME points at" : "runtimeDir is";
|
|
1434
|
-
throw new Error(`${source} ${dir}, which does not exist.`);
|
|
1435
|
-
}
|
|
1436
|
-
return dir;
|
|
1437
|
-
}
|
|
1438
|
-
async function installRuntime(output, runtimeDir) {
|
|
1439
|
-
const resolved = Object.entries(RUNTIME_ARCHIVES).map(([sdk, archives]) => {
|
|
1440
|
-
const present = archives.filter((a) => existsSync4(join2(runtimeDir, a)));
|
|
1441
|
-
if (present.length === 0) {
|
|
1442
|
-
throw new Error(`${runtimeDir} has none of ${archives.join(", ")}. ` + `Run \`zig build build-ios-all\` in packages/zig and point at its zig-out/lib.`);
|
|
1443
|
-
}
|
|
1444
|
-
return { sdk, archives, present };
|
|
1445
|
-
});
|
|
1446
|
-
const dest = join2(output, "Runtime");
|
|
1447
|
-
rmSync2(dest, { recursive: true, force: true });
|
|
1448
|
-
for (const { sdk, archives, present } of resolved) {
|
|
1449
|
-
const sdkDir = join2(dest, sdk);
|
|
1450
|
-
mkdirSync2(sdkDir, { recursive: true });
|
|
1451
|
-
const target = join2(sdkDir, "libcraft-ios.a");
|
|
1452
|
-
if (present.length === 1) {
|
|
1453
|
-
const missing = archives.filter((a) => !present.includes(a));
|
|
1454
|
-
if (missing.length > 0) {
|
|
1455
|
-
console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
|
|
1456
|
-
}
|
|
1457
|
-
cpSync2(join2(runtimeDir, present[0]), target);
|
|
1458
|
-
} else {
|
|
1459
|
-
await $`lipo -create ${present.map((a) => join2(runtimeDir, a))} -output ${target}`.quiet();
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
return true;
|
|
1463
|
-
}
|
|
1464
|
-
async function refreshRuntime(output, override) {
|
|
1465
|
-
if (!existsSync4(join2(output, "Runtime")))
|
|
1466
|
-
return;
|
|
1467
|
-
const dir = resolveRuntimeDir(override);
|
|
1468
|
-
if (!dir) {
|
|
1469
|
-
console.log(" Keeping the Zig runtime installed at init (no runtime directory configured)");
|
|
1470
|
-
return;
|
|
1471
|
-
}
|
|
1472
|
-
await installRuntime(output, dir);
|
|
1473
|
-
console.log(" Refreshed the Zig runtime from", dir);
|
|
1474
|
-
}
|
|
1475
|
-
function productName(name) {
|
|
1476
|
-
const words = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
1477
|
-
const joined = words.map((word) => word[0].toUpperCase() + word.slice(1)).join("");
|
|
1478
|
-
if (!joined)
|
|
1479
|
-
return "Craft";
|
|
1480
|
-
return /^\d/.test(joined) ? `App${joined}` : joined;
|
|
1481
|
-
}
|
|
1482
|
-
async function init(options) {
|
|
1483
|
-
const { name: displayName, bundleId, teamId, output } = options;
|
|
1484
|
-
const name = productName(displayName);
|
|
1485
|
-
console.log(`
|
|
1486
|
-
\u26A1 Initializing Craft iOS project: ${displayName}`);
|
|
1487
|
-
console.log(` Output: ${output}
|
|
1488
|
-
`);
|
|
1489
|
-
const dirs = [output, join2(output, "Sources"), join2(output, "Shared"), join2(output, "dist")];
|
|
1490
|
-
if (options.config?.enableWatchApp)
|
|
1491
|
-
dirs.push(join2(output, "WatchApp"), join2(output, "WatchExtension"));
|
|
1492
|
-
for (const dir of dirs) {
|
|
1493
|
-
if (!existsSync4(dir)) {
|
|
1494
|
-
mkdirSync2(dir, { recursive: true });
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
const finalBundleId = bundleId || `com.craft.${name.toLowerCase()}`;
|
|
1498
|
-
const bundleIdPrefix = finalBundleId.split(".").slice(0, -1).join(".");
|
|
1499
|
-
const given = Object.fromEntries(Object.entries(options.config ?? {}).filter(([, value]) => value !== undefined));
|
|
1500
|
-
const config = {
|
|
1501
|
-
...DEFAULT_CONFIG,
|
|
1502
|
-
appName: displayName,
|
|
1503
|
-
bundleId: finalBundleId,
|
|
1504
|
-
teamId: teamId || "",
|
|
1505
|
-
...given
|
|
1506
|
-
};
|
|
1507
|
-
if (config.enableBackgroundLocation)
|
|
1508
|
-
config.enableGeolocation = true;
|
|
1509
|
-
writeFileSync3(join2(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
1510
|
-
const swiftTemplate = readFileSync3(join2(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
|
|
1511
|
-
const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
1512
|
-
writeFileSync3(join2(output, "Sources", `${name}App.swift`), swiftSource);
|
|
1513
|
-
const infoPlistTemplate = readFileSync3(join2(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
|
|
1514
|
-
const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, renderAppearance(config).interfaceStyle).replace(/\{\{STATUS_BAR_STYLE\}\}/g, renderAppearance(config).statusBarStyle).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>
|
|
1515
|
-
<true/>
|
|
1516
|
-
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
|
|
1517
|
-
<true/>` : "");
|
|
1518
|
-
writeFileSync3(join2(output, "Info.plist"), infoPlist);
|
|
1519
|
-
const projectYmlTemplate = readFileSync3(join2(TEMPLATES_DIR, "project.yml.template"), "utf-8");
|
|
1520
|
-
const nativeDependencies = [];
|
|
1521
|
-
if (config.enableLiveActivities)
|
|
1522
|
-
nativeDependencies.push(` - target: ${name}LiveActivity`);
|
|
1523
|
-
if (config.enableWatchApp)
|
|
1524
|
-
nativeDependencies.push(` - target: ${name}Watch
|
|
1525
|
-
embed: true`);
|
|
1526
|
-
const nativeTargets = [];
|
|
1527
|
-
if (config.enableLiveActivities) {
|
|
1528
|
-
nativeTargets.push(` ${name}LiveActivity:
|
|
1529
|
-
type: app-extension
|
|
1530
|
-
platform: iOS
|
|
1531
|
-
deploymentTarget: "16.2"
|
|
1532
|
-
sources:
|
|
1533
|
-
- WidgetExtension
|
|
1534
|
-
- Shared
|
|
1535
|
-
settings:
|
|
1536
|
-
INFOPLIST_FILE: WidgetExtension/Info.plist
|
|
1537
|
-
PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.liveactivity
|
|
1538
|
-
SWIFT_VERSION: "5.0"
|
|
1539
|
-
APPLICATION_EXTENSION_API_ONLY: YES
|
|
1540
|
-
SKIP_INSTALL: YES`);
|
|
1541
|
-
}
|
|
1542
|
-
if (config.enableWatchApp) {
|
|
1543
|
-
nativeTargets.push(` ${name}Watch:
|
|
1544
|
-
type: application.watchapp2
|
|
1545
|
-
platform: watchOS
|
|
1546
|
-
deploymentTarget: "${config.watchosVersion || "9.0"}"
|
|
1547
|
-
sources:
|
|
1548
|
-
- WatchApp
|
|
1549
|
-
settings:
|
|
1550
|
-
INFOPLIST_FILE: WatchApp/Info.plist
|
|
1551
|
-
CODE_SIGN_ENTITLEMENTS: WatchApp/Watch.entitlements
|
|
1552
|
-
PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.watchkitapp
|
|
1553
|
-
SWIFT_VERSION: "5.0"
|
|
1554
|
-
SKIP_INSTALL: YES
|
|
1555
|
-
dependencies:
|
|
1556
|
-
- target: ${name}WatchExtension
|
|
1557
|
-
${name}WatchExtension:
|
|
1558
|
-
type: watchkit2-extension
|
|
1559
|
-
platform: watchOS
|
|
1560
|
-
deploymentTarget: "${config.watchosVersion || "9.0"}"
|
|
1561
|
-
sources:
|
|
1562
|
-
- WatchExtension
|
|
1563
|
-
settings:
|
|
1564
|
-
INFOPLIST_FILE: WatchExtension/Info.plist
|
|
1565
|
-
CODE_SIGN_ENTITLEMENTS: WatchExtension/Watch.entitlements
|
|
1566
|
-
PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.watchkitapp.watchkitextension
|
|
1567
|
-
SWIFT_VERSION: "5.0"
|
|
1568
|
-
APPLICATION_EXTENSION_API_ONLY: YES
|
|
1569
|
-
SKIP_INSTALL: YES`);
|
|
1570
|
-
}
|
|
1571
|
-
const runtimeDir = resolveRuntimeDir(options.runtimeDir);
|
|
1572
|
-
const hasRuntime = runtimeDir ? await installRuntime(output, runtimeDir) : false;
|
|
1573
|
-
if (hasRuntime) {
|
|
1574
|
-
console.log(" Linked the Zig runtime from", runtimeDir);
|
|
1575
|
-
} else {
|
|
1576
|
-
rmSync2(join2(output, "Runtime"), { recursive: true, force: true });
|
|
1577
|
-
}
|
|
1578
|
-
const projectYml = projectYmlTemplate.replace(/\{\{CRAFT_RUNTIME_SETTINGS\}\}/g, hasRuntime ? renderRuntimeSettings() : "").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:
|
|
1579
|
-
${nativeDependencies.join(`
|
|
1580
|
-
`)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
|
|
1581
|
-
`));
|
|
1582
|
-
writeFileSync3(join2(output, "project.yml"), projectYml);
|
|
1583
|
-
writeFileSync3(join2(output, "Craft.entitlements"), renderEntitlements(config));
|
|
1584
|
-
writeFileSync3(join2(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
|
|
1585
|
-
renderAssetCatalog(output, config);
|
|
1586
|
-
cpSync2(join2(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join2(output, "Shared", "CraftActivityAttributes.swift"));
|
|
1587
|
-
if (config.enableLiveActivities) {
|
|
1588
|
-
mkdirSync2(join2(output, "WidgetExtension"), { recursive: true });
|
|
1589
|
-
const widgetSource = readFileSync3(join2(TEMPLATES_DIR, "CraftLiveActivityWidget.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
|
|
1590
|
-
writeFileSync3(join2(output, "WidgetExtension", `${name}LiveActivity.swift`), widgetSource);
|
|
1591
|
-
const widgetInfo = readFileSync3(join2(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
|
|
1592
|
-
writeFileSync3(join2(output, "WidgetExtension", "Info.plist"), widgetInfo);
|
|
1593
|
-
}
|
|
1594
|
-
if (config.enableWatchApp) {
|
|
1595
|
-
const watchSource = readFileSync3(join2(TEMPLATES_DIR, "CraftWatchApp.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
|
|
1596
|
-
writeFileSync3(join2(output, "WatchExtension", `${name}WatchApp.swift`), watchSource);
|
|
1597
|
-
const watchInfo = readFileSync3(join2(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
1598
|
-
writeFileSync3(join2(output, "WatchApp", "Info.plist"), watchInfo);
|
|
1599
|
-
writeFileSync3(join2(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
1600
|
-
const watchExtensionInfo = readFileSync3(join2(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
1601
|
-
writeFileSync3(join2(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
|
|
1602
|
-
writeFileSync3(join2(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
1603
|
-
}
|
|
1604
|
-
const placeholderHtml = `<!DOCTYPE html>
|
|
1605
|
-
<html>
|
|
1606
|
-
<head>
|
|
1607
|
-
<meta charset="UTF-8">
|
|
1608
|
-
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
1609
|
-
<title>${xmlEscape(displayName)}</title>
|
|
1610
|
-
<style>
|
|
1611
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1612
|
-
body {
|
|
1613
|
-
font-family: -apple-system, system-ui, sans-serif;
|
|
1614
|
-
background: ${config.backgroundColor};
|
|
1615
|
-
color: white;
|
|
1616
|
-
min-height: 100vh;
|
|
1617
|
-
display: flex;
|
|
1618
|
-
justify-content: center;
|
|
1619
|
-
align-items: center;
|
|
1620
|
-
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
|
1621
|
-
}
|
|
1622
|
-
.container { text-align: center; padding: 2rem; }
|
|
1623
|
-
h1 { font-size: 2.5rem; margin-bottom: 1rem; }
|
|
1624
|
-
p { opacity: 0.7; }
|
|
1625
|
-
.ready { color: #4ade80; font-size: 0.9rem; margin-top: 2rem; }
|
|
1626
|
-
</style>
|
|
1627
|
-
</head>
|
|
1628
|
-
<body>
|
|
1629
|
-
<div class="container">
|
|
1630
|
-
<h1>\u26A1 ${xmlEscape(displayName)}</h1>
|
|
1631
|
-
<p>Built with Craft iOS</p>
|
|
1632
|
-
<p class="ready" id="status">Waiting for Craft bridge...</p>
|
|
1633
|
-
</div>
|
|
1634
|
-
<script>
|
|
1635
|
-
window.addEventListener('craftReady', (e) => {
|
|
1636
|
-
document.getElementById('status').textContent = \`\u2713 Craft bridge ready (platform: \${e.detail.platform})\`;
|
|
1637
|
-
console.log('Craft capabilities:', e.detail.capabilities);
|
|
1638
|
-
});
|
|
1639
|
-
</script>
|
|
1640
|
-
</body>
|
|
1641
|
-
</html>`;
|
|
1642
|
-
writeFileSync3(join2(output, "dist", "index.html"), placeholderHtml);
|
|
1643
|
-
console.log("\u2705 Project initialized");
|
|
1644
|
-
console.log("");
|
|
1645
|
-
console.log("Next steps:");
|
|
1646
|
-
console.log(` 1. cd ${output}`);
|
|
1647
|
-
console.log(" 2. Add your web content to dist/index.html");
|
|
1648
|
-
console.log(" 3. Run: craft ios build");
|
|
1649
|
-
console.log(" 4. Run: craft ios open");
|
|
1650
|
-
console.log("");
|
|
1651
|
-
}
|
|
1652
|
-
async function build(options) {
|
|
1653
|
-
const { htmlPath, devServer, output, generateProject = true } = options;
|
|
1654
|
-
console.log(`
|
|
1655
|
-
\uD83D\uDCE6 Building Craft iOS project...`);
|
|
1656
|
-
const configPath = join2(output, "craft.config.json");
|
|
1657
|
-
if (!existsSync4(configPath)) {
|
|
1658
|
-
throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
|
|
1659
|
-
}
|
|
1660
|
-
const config = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
1661
|
-
if (devServer) {
|
|
1662
|
-
config.devServerURL = devServer;
|
|
1663
|
-
const origin = new URL(devServer).origin;
|
|
1664
|
-
config.trustedOrigins = [...new Set([...config.trustedOrigins ?? [], origin])];
|
|
1665
|
-
writeFileSync3(configPath, JSON.stringify(config, null, 2));
|
|
1666
|
-
console.log(` Dev server: ${devServer}`);
|
|
1667
|
-
}
|
|
1668
|
-
if (htmlPath) {
|
|
1669
|
-
syncWebAssets(htmlPath, output);
|
|
1670
|
-
console.log(` Synced: ${htmlPath} \u2192 dist/`);
|
|
1671
|
-
}
|
|
1672
|
-
await refreshRuntime(output, options.runtimeDir);
|
|
1673
|
-
if (!generateProject)
|
|
1674
|
-
return;
|
|
1675
|
-
try {
|
|
1676
|
-
const result = await $`which xcodegen`.quiet();
|
|
1677
|
-
if (result.exitCode === 0) {
|
|
1678
|
-
console.log(" Running xcodegen...");
|
|
1679
|
-
await $`cd ${output} && xcodegen generate`.quiet();
|
|
1680
|
-
console.log(`\u2705 Xcode project created: ${productName(config.appName)}.xcodeproj`);
|
|
1681
|
-
} else {
|
|
1682
|
-
throw new Error("xcodegen not found");
|
|
1683
|
-
}
|
|
1684
|
-
} catch (error) {
|
|
1685
|
-
throw new Error("Unable to generate the Xcode project. Install xcodegen with `brew install xcodegen`.", {
|
|
1686
|
-
cause: error
|
|
1687
|
-
});
|
|
1688
|
-
}
|
|
1689
|
-
console.log("");
|
|
1690
|
-
}
|
|
1691
|
-
async function open(options) {
|
|
1692
|
-
const { output } = options;
|
|
1693
|
-
const files = readdirSync2(output);
|
|
1694
|
-
const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
|
|
1695
|
-
if (!xcodeproj) {
|
|
1696
|
-
throw new Error(`No Xcode project found in ${output}. Run 'craft ios build' first.`);
|
|
1697
|
-
}
|
|
1698
|
-
const projectPath = join2(output, xcodeproj);
|
|
1699
|
-
console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
|
|
1700
|
-
await $`open ${projectPath}`;
|
|
1701
|
-
}
|
|
1702
|
-
function orderSimulators(devices) {
|
|
1703
|
-
return devices.filter((device) => device.runtime.startsWith("iOS")).sort((left, right) => {
|
|
1704
|
-
const booted = Number(right.state === "Booted") - Number(left.state === "Booted");
|
|
1705
|
-
if (booted !== 0)
|
|
1706
|
-
return booted;
|
|
1707
|
-
const phone = Number(right.name.startsWith("iPhone")) - Number(left.name.startsWith("iPhone"));
|
|
1708
|
-
if (phone !== 0)
|
|
1709
|
-
return phone;
|
|
1710
|
-
return right.runtime.localeCompare(left.runtime);
|
|
1711
|
-
});
|
|
1712
|
-
}
|
|
1713
|
-
async function pickSimulator() {
|
|
1714
|
-
const listed = await $`xcrun simctl list devices available --json`.quiet().nothrow();
|
|
1715
|
-
if (listed.exitCode !== 0)
|
|
1716
|
-
return null;
|
|
1717
|
-
let parsed;
|
|
1718
|
-
try {
|
|
1719
|
-
parsed = JSON.parse(listed.stdout.toString());
|
|
1720
|
-
} catch {
|
|
1721
|
-
return null;
|
|
1722
|
-
}
|
|
1723
|
-
const devices = [];
|
|
1724
|
-
for (const [runtime, list] of Object.entries(parsed.devices ?? {})) {
|
|
1725
|
-
for (const device of list)
|
|
1726
|
-
devices.push({ ...device, runtime: runtime.split(".").pop() ?? runtime });
|
|
1727
|
-
}
|
|
1728
|
-
return orderSimulators(devices)[0] ?? null;
|
|
1729
|
-
}
|
|
1730
|
-
async function bootSimulator(device) {
|
|
1731
|
-
if (device.state !== "Booted")
|
|
1732
|
-
await $`xcrun simctl boot ${device.udid}`.quiet().nothrow();
|
|
1733
|
-
await $`xcrun simctl bootstatus ${device.udid}`.quiet().nothrow();
|
|
1734
|
-
}
|
|
1735
|
-
async function showSimulator() {
|
|
1736
|
-
const selected = await $`xcode-select -p`.quiet().nothrow();
|
|
1737
|
-
const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
|
|
1738
|
-
if (developer) {
|
|
1739
|
-
const app = join2(developer, "Applications", "Simulator.app");
|
|
1740
|
-
if (existsSync4(app)) {
|
|
1741
|
-
await $`open ${app}`.quiet().nothrow();
|
|
1742
|
-
return;
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
await $`open -a Simulator`.quiet().nothrow();
|
|
1746
|
-
}
|
|
1747
|
-
async function run(options) {
|
|
1748
|
-
const { simulator, output } = options;
|
|
1749
|
-
await build({ output });
|
|
1750
|
-
const files = readdirSync2(output);
|
|
1751
|
-
const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
|
|
1752
|
-
if (!xcodeproj) {
|
|
1753
|
-
throw new Error(`No Xcode project found in ${output}`);
|
|
1754
|
-
}
|
|
1755
|
-
const projectPath = join2(output, xcodeproj);
|
|
1756
|
-
const appName = xcodeproj.replace(".xcodeproj", "");
|
|
1757
|
-
const configPath = join2(output, "craft.config.json");
|
|
1758
|
-
if (!existsSync4(configPath))
|
|
1759
|
-
throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
|
|
1760
|
-
const config = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
1761
|
-
if (simulator) {
|
|
1762
|
-
console.log("\uD83D\uDCF1 Building and running on simulator...");
|
|
1763
|
-
const device = await pickSimulator();
|
|
1764
|
-
if (!device) {
|
|
1765
|
-
throw new Error("No iOS simulator is available. Install a runtime with " + "`xcodebuild -downloadPlatform iOS`, then try again.");
|
|
1766
|
-
}
|
|
1767
|
-
console.log(` Device: ${device.name} (${device.runtime})`);
|
|
1768
|
-
try {
|
|
1769
|
-
await bootSimulator(device);
|
|
1770
|
-
const derivedData = join2(output, "build");
|
|
1771
|
-
const product = join2(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
|
|
1772
|
-
await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
|
|
1773
|
-
if (!existsSync4(product)) {
|
|
1774
|
-
throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
|
|
1775
|
-
}
|
|
1776
|
-
await $`xcrun simctl install ${device.udid} ${product}`;
|
|
1777
|
-
await showSimulator();
|
|
1778
|
-
await $`xcrun simctl launch ${device.udid} ${config.bundleId}`;
|
|
1779
|
-
console.log(`\u2705 ${config.appName} is running on ${device.name}`);
|
|
1780
|
-
} catch (error) {
|
|
1781
|
-
throw new Error(`iOS simulator run failed for ${projectPath}`, { cause: error });
|
|
1782
|
-
}
|
|
1783
|
-
} else {
|
|
1784
|
-
console.log("\uD83D\uDCF1 Opening Xcode for device deployment...");
|
|
1785
|
-
await $`open ${projectPath}`;
|
|
1786
|
-
console.log("");
|
|
1787
|
-
console.log("In Xcode:");
|
|
1788
|
-
console.log(" 1. Select your Team in Signing & Capabilities");
|
|
1789
|
-
console.log(" 2. Connect your iPhone");
|
|
1790
|
-
console.log(" 3. Select your device");
|
|
1791
|
-
console.log(" 4. Click Run (\u25B6\uFE0F)");
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
var $, TEMPLATES_DIR, DEFAULT_CONFIG, RUNTIME_ARCHIVES;
|
|
1795
|
-
var init_src = __esm(() => {
|
|
1796
|
-
({ $ } = globalThis.Bun);
|
|
1797
|
-
TEMPLATES_DIR = join2(dirname(import.meta.dir), "templates");
|
|
1798
|
-
DEFAULT_CONFIG = {
|
|
1799
|
-
version: "1.0.0",
|
|
1800
|
-
buildNumber: "1",
|
|
1801
|
-
darkMode: true,
|
|
1802
|
-
backgroundColor: "#0b1712",
|
|
1803
|
-
enableSpeechRecognition: false,
|
|
1804
|
-
enableHaptics: false,
|
|
1805
|
-
enableShare: false,
|
|
1806
|
-
enableCamera: false,
|
|
1807
|
-
enableBiometric: false,
|
|
1808
|
-
enablePushNotifications: false,
|
|
1809
|
-
enableSecureStorage: false,
|
|
1810
|
-
enableGeolocation: false,
|
|
1811
|
-
enableClipboard: false,
|
|
1812
|
-
enableContacts: false,
|
|
1813
|
-
enableCalendar: false,
|
|
1814
|
-
enableLocalNotifications: false,
|
|
1815
|
-
enableInAppPurchase: false,
|
|
1816
|
-
enableKeepAwake: false,
|
|
1817
|
-
enableOrientationLock: false,
|
|
1818
|
-
enableDeepLinks: false,
|
|
1819
|
-
enableQRScanner: false,
|
|
1820
|
-
enableFilePicker: false,
|
|
1821
|
-
enableFileDownload: false,
|
|
1822
|
-
enableSocialAuth: false,
|
|
1823
|
-
enableAudioRecording: false,
|
|
1824
|
-
enableVideoRecording: false,
|
|
1825
|
-
enableMotionSensors: false,
|
|
1826
|
-
enableLocalDatabase: false,
|
|
1827
|
-
enableBluetooth: false,
|
|
1828
|
-
enableNFC: false,
|
|
1829
|
-
enableHealthKit: false,
|
|
1830
|
-
enableLiveActivities: false,
|
|
1831
|
-
enableWatchApp: false,
|
|
1832
|
-
enableBackgroundLocation: false,
|
|
1833
|
-
enableBackgroundTasks: false,
|
|
1834
|
-
enableScreenCapture: false,
|
|
1835
|
-
enablePDFViewer: false,
|
|
1836
|
-
enableAR: false,
|
|
1837
|
-
enableMLKit: false,
|
|
1838
|
-
iosVersion: "16.0",
|
|
1839
|
-
watchosVersion: "9.0",
|
|
1840
|
-
teamId: "",
|
|
1841
|
-
trustedOrigins: [],
|
|
1842
|
-
associatedDomains: [],
|
|
1843
|
-
appGroups: [],
|
|
1844
|
-
orientations: ["portrait"],
|
|
1845
|
-
deviceFamilies: ["iphone", "ipad"]
|
|
1846
|
-
};
|
|
1847
|
-
RUNTIME_ARCHIVES = {
|
|
1848
|
-
device: ["libcraft-ios.a"],
|
|
1849
|
-
simulator: ["libcraft-ios-simulator-arm64.a", "libcraft-ios-simulator-x64.a"]
|
|
1850
|
-
};
|
|
1851
|
-
});
|
|
1852
|
-
|
|
1853
|
-
// dist/android/src/index.js
|
|
1854
|
-
var exports_src2 = {};
|
|
1855
|
-
__export(exports_src2, {
|
|
1856
|
-
syncAndroidWebAssets: () => syncAndroidWebAssets,
|
|
1857
|
-
run: () => run2,
|
|
1858
|
-
resolveRuntimeDir: () => resolveRuntimeDir2,
|
|
1859
|
-
renderAndroidPermissions: () => renderAndroidPermissions,
|
|
1860
|
-
renderAndroidDeepLinks: () => renderAndroidDeepLinks,
|
|
1861
|
-
open: () => open2,
|
|
1862
|
-
installRuntime: () => installRuntime2,
|
|
1863
|
-
init: () => init2,
|
|
1864
|
-
build: () => build2,
|
|
1865
|
-
ANDROID_MIN_SDK: () => ANDROID_MIN_SDK
|
|
1866
|
-
});
|
|
1867
|
-
import { cpSync as cpSync3, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
1868
|
-
import { dirname as dirname2, extname, isAbsolute, join as join3, relative, resolve as resolve2, sep } from "path";
|
|
1869
|
-
function renderAndroidPromiseRuntime(indent = "") {
|
|
1870
|
-
return ANDROID_PROMISE_RUNTIME.trim().split(`
|
|
1871
|
-
`).map((line) => `${indent}${line}`).join(`
|
|
1872
|
-
`);
|
|
1873
|
-
}
|
|
1874
|
-
function escapeXml(value) {
|
|
1875
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1876
|
-
}
|
|
1877
|
-
function escapeKotlinString(value) {
|
|
1878
|
-
return value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("$", "\\$").replaceAll("\r", "\\r").replaceAll(`
|
|
1879
|
-
`, "\\n");
|
|
1880
|
-
}
|
|
1881
|
-
function generatedPackageSegment(name) {
|
|
1882
|
-
const normalized = name.toLowerCase().replace(/[^a-z0-9_]/g, "");
|
|
1883
|
-
if (!normalized)
|
|
1884
|
-
return "app";
|
|
1885
|
-
return /^[a-z_]/.test(normalized) ? normalized : `app${normalized}`;
|
|
1886
|
-
}
|
|
1887
|
-
function generatedGradleProjectName(name) {
|
|
1888
|
-
const invalidCharacters = ["/", "\\", ":", "<", ">", '"', "?", "*", "|"];
|
|
1889
|
-
const normalized = invalidCharacters.reduce((value, character) => value.replaceAll(character, "-"), name).trim();
|
|
1890
|
-
return normalized || "craft-app";
|
|
1891
|
-
}
|
|
1892
|
-
function requireRegularFile(path2, label) {
|
|
1893
|
-
if (!existsSync5(path2))
|
|
1894
|
-
throw new Error(`${label} not found: ${path2}`);
|
|
1895
|
-
if (!statSync2(path2).isFile())
|
|
1896
|
-
throw new Error(`${label} must be a file: ${path2}`);
|
|
1897
|
-
}
|
|
1898
|
-
function containsPath(parent, candidate) {
|
|
1899
|
-
const relativePath = relative(parent, candidate);
|
|
1900
|
-
return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
|
|
1901
|
-
}
|
|
1902
|
-
function validateGoogleServicesFile(path2, packageName) {
|
|
1903
|
-
requireRegularFile(path2, "Google services file");
|
|
1904
|
-
let document;
|
|
1905
|
-
try {
|
|
1906
|
-
document = JSON.parse(readFileSync4(path2, "utf8"));
|
|
1907
|
-
} catch (error) {
|
|
1908
|
-
throw new Error(`Google services file must contain valid JSON: ${path2}`, { cause: error });
|
|
1909
|
-
}
|
|
1910
|
-
const matchingClient = document.client?.some((client) => {
|
|
1911
|
-
return client.client_info?.android_client_info?.package_name === packageName;
|
|
1912
|
-
});
|
|
1913
|
-
if (!matchingClient) {
|
|
1914
|
-
throw new Error(`Google services file has no client for Android package ${packageName}: ${path2}`);
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1917
|
-
function androidWebUrl(value, field) {
|
|
1918
|
-
let url;
|
|
1919
|
-
try {
|
|
1920
|
-
url = new URL(value);
|
|
1921
|
-
} catch {
|
|
1922
|
-
throw new Error(`Invalid ${field}: ${value}`);
|
|
1923
|
-
}
|
|
1924
|
-
const localDevelopment = url.protocol === "http:" && LOCAL_DEVELOPMENT_HOSTS.has(url.hostname);
|
|
1925
|
-
if (url.protocol !== "https:" && !localDevelopment || url.username || url.password) {
|
|
1926
|
-
throw new Error(`${field} must use HTTPS or local HTTP without credentials: ${value}`);
|
|
1927
|
-
}
|
|
1928
|
-
return url;
|
|
1929
|
-
}
|
|
1930
|
-
function normalizeAndroidNetworkConfig(config) {
|
|
1931
|
-
const schemes = config.urlSchemes?.map((value) => value.trim().toLowerCase()) ?? [];
|
|
1932
|
-
if (schemes.some((value) => !/^[a-z][a-z0-9+.-]*$/.test(value))) {
|
|
1933
|
-
throw new Error("Android deep-link schemes must be valid URI schemes");
|
|
1934
|
-
}
|
|
1935
|
-
config.urlSchemes = [...new Set(schemes)];
|
|
1936
|
-
if (config.enableDeepLinks && config.urlSchemes.length === 0) {
|
|
1937
|
-
throw new Error("Android deep links require at least one URL scheme");
|
|
1938
|
-
}
|
|
1939
|
-
const trustedOrigins = (config.trustedOrigins ?? []).map((value) => {
|
|
1940
|
-
return androidWebUrl(value, "Android trusted origin").origin;
|
|
1941
|
-
});
|
|
1942
|
-
if (config.devServerURL) {
|
|
1943
|
-
const devServer = androidWebUrl(config.devServerURL, "Android dev server URL");
|
|
1944
|
-
config.devServerURL = devServer.toString();
|
|
1945
|
-
trustedOrigins.push(devServer.origin);
|
|
1946
|
-
}
|
|
1947
|
-
config.trustedOrigins = [...new Set(trustedOrigins)];
|
|
1948
|
-
}
|
|
1949
|
-
function validateAndroidConfig(config) {
|
|
1950
|
-
if (!config.appName.trim())
|
|
1951
|
-
throw new Error("Android app name must not be empty");
|
|
1952
|
-
const packageSegments = config.packageName.split(".");
|
|
1953
|
-
if (packageSegments.length < 2 || packageSegments.some((segment) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment) || KOTLIN_KEYWORDS.has(segment))) {
|
|
1954
|
-
throw new Error(`Invalid Android package name: ${config.packageName}`);
|
|
1955
|
-
}
|
|
1956
|
-
if (!/^#(?:[\dA-F]{3,4}|[\dA-F]{6}|[\dA-F]{8})$/i.test(config.backgroundColor ?? "")) {
|
|
1957
|
-
throw new Error(`Invalid Android background color: ${config.backgroundColor}`);
|
|
1958
|
-
}
|
|
1959
|
-
for (const [name, value] of [
|
|
1960
|
-
["versionCode", config.versionCode],
|
|
1961
|
-
["minSdk", config.minSdk],
|
|
1962
|
-
["compileSdk", config.compileSdk],
|
|
1963
|
-
["targetSdk", config.targetSdk]
|
|
1964
|
-
]) {
|
|
1965
|
-
if (!Number.isInteger(value) || Number(value) < 1) {
|
|
1966
|
-
throw new Error(`Android ${name} must be a positive integer`);
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
if (Number(config.minSdk) < ANDROID_MIN_SDK) {
|
|
1970
|
-
throw new Error(`Android minSdk must be at least ${ANDROID_MIN_SDK} (Android 8.0) for the Craft runtime`);
|
|
1971
|
-
}
|
|
1972
|
-
if (Number(config.minSdk) > Number(config.targetSdk)) {
|
|
1973
|
-
throw new Error("Android minSdk must not exceed targetSdk");
|
|
1974
|
-
}
|
|
1975
|
-
if (Number(config.targetSdk) > Number(config.compileSdk)) {
|
|
1976
|
-
throw new Error("Android targetSdk must not exceed compileSdk");
|
|
1977
|
-
}
|
|
1978
|
-
}
|
|
1979
|
-
function writeAndroidConfig(output, config) {
|
|
1980
|
-
writeFileSync4(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
1981
|
-
const runtimeConfig = { ...config };
|
|
1982
|
-
delete runtimeConfig.appIconPath;
|
|
1983
|
-
delete runtimeConfig.googleServicesFile;
|
|
1984
|
-
writeFileSync4(join3(output, "app/src/main/assets/craft.config.json"), JSON.stringify(runtimeConfig, null, 2));
|
|
1985
|
-
}
|
|
1986
|
-
function syncAndroidWebAssets(source, output) {
|
|
1987
|
-
const sourcePath = resolve2(source);
|
|
1988
|
-
if (!existsSync5(sourcePath))
|
|
1989
|
-
throw new Error(`Web asset path not found: ${source}`);
|
|
1990
|
-
const sourceStat = statSync2(sourcePath);
|
|
1991
|
-
if (!sourceStat.isDirectory() && !sourceStat.isFile()) {
|
|
1992
|
-
throw new Error(`Web asset path must be a file or directory: ${source}`);
|
|
1993
|
-
}
|
|
1994
|
-
const assetsDir = resolve2(output, "app/src/main/assets");
|
|
1995
|
-
if (containsPath(sourcePath, assetsDir) || containsPath(assetsDir, sourcePath)) {
|
|
1996
|
-
throw new Error(`Web asset source must not overlap generated asset directory: ${source}`);
|
|
1997
|
-
}
|
|
1998
|
-
if (sourceStat.isDirectory()) {
|
|
1999
|
-
requireRegularFile(join3(sourcePath, "index.html"), "Web asset directory entry point");
|
|
2000
|
-
}
|
|
2001
|
-
const configPath = join3(assetsDir, "craft.config.json");
|
|
2002
|
-
const config = existsSync5(configPath) ? readFileSync4(configPath) : undefined;
|
|
2003
|
-
rmSync3(assetsDir, { recursive: true, force: true });
|
|
2004
|
-
mkdirSync3(assetsDir, { recursive: true });
|
|
2005
|
-
if (sourceStat.isDirectory())
|
|
2006
|
-
cpSync3(sourcePath, assetsDir, { recursive: true });
|
|
2007
|
-
else
|
|
2008
|
-
cpSync3(sourcePath, join3(assetsDir, "index.html"));
|
|
2009
|
-
if (!existsSync5(join3(assetsDir, "index.html")))
|
|
2010
|
-
throw new Error(`Web asset directory must contain index.html: ${source}`);
|
|
2011
|
-
if (config)
|
|
2012
|
-
writeFileSync4(configPath, config);
|
|
2013
|
-
}
|
|
2014
|
-
function renderAndroidPermissions(config) {
|
|
2015
|
-
const permissions = new Set(["android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE"]);
|
|
2016
|
-
if (config.enableSpeechRecognition)
|
|
2017
|
-
permissions.add("android.permission.RECORD_AUDIO");
|
|
2018
|
-
if (config.enableCamera)
|
|
2019
|
-
permissions.add("android.permission.CAMERA");
|
|
2020
|
-
if (config.enableHaptics)
|
|
2021
|
-
permissions.add("android.permission.VIBRATE");
|
|
2022
|
-
if (config.enableBiometric)
|
|
2023
|
-
permissions.add("android.permission.USE_BIOMETRIC");
|
|
2024
|
-
if (config.enableGeolocation || config.enableBackgroundLocation) {
|
|
2025
|
-
permissions.add("android.permission.ACCESS_FINE_LOCATION");
|
|
2026
|
-
permissions.add("android.permission.ACCESS_COARSE_LOCATION");
|
|
2027
|
-
}
|
|
2028
|
-
if (config.enableBackgroundLocation) {
|
|
2029
|
-
permissions.add("android.permission.ACCESS_BACKGROUND_LOCATION");
|
|
2030
|
-
permissions.add("android.permission.FOREGROUND_SERVICE");
|
|
2031
|
-
permissions.add("android.permission.FOREGROUND_SERVICE_LOCATION");
|
|
2032
|
-
}
|
|
2033
|
-
if (config.enablePushNotifications)
|
|
2034
|
-
permissions.add("android.permission.POST_NOTIFICATIONS");
|
|
2035
|
-
if (config.enableKeepAwake)
|
|
2036
|
-
permissions.add("android.permission.WAKE_LOCK");
|
|
2037
|
-
if (config.enableHealthConnect) {
|
|
2038
|
-
permissions.add("android.permission.health.READ_STEPS");
|
|
2039
|
-
permissions.add("android.permission.health.READ_HEART_RATE");
|
|
2040
|
-
permissions.add("android.permission.health.READ_ACTIVE_CALORIES_BURNED");
|
|
2041
|
-
permissions.add("android.permission.health.READ_DISTANCE");
|
|
2042
|
-
permissions.add("android.permission.health.READ_EXERCISE");
|
|
2043
|
-
permissions.add("android.permission.health.WRITE_EXERCISE");
|
|
2044
|
-
permissions.add("android.permission.health.WRITE_ACTIVE_CALORIES_BURNED");
|
|
2045
|
-
permissions.add("android.permission.health.WRITE_DISTANCE");
|
|
2046
|
-
}
|
|
2047
|
-
return [...permissions].map((permission) => ` <uses-permission android:name="${permission}" />`).join(`
|
|
2048
|
-
`);
|
|
2049
|
-
}
|
|
2050
|
-
function renderAndroidDeepLinks(config) {
|
|
2051
|
-
if (!config.enableDeepLinks)
|
|
2052
|
-
return "";
|
|
2053
|
-
const schemes = [...new Set(config.urlSchemes?.map((value) => value.trim()).filter((value) => /^[a-z][a-z0-9+.-]*$/i.test(value)) ?? [])];
|
|
2054
|
-
return schemes.map((scheme) => ` <intent-filter>
|
|
2055
|
-
<action android:name="android.intent.action.VIEW" />
|
|
2056
|
-
<category android:name="android.intent.category.DEFAULT" />
|
|
2057
|
-
<category android:name="android.intent.category.BROWSABLE" />
|
|
2058
|
-
<data android:scheme="${scheme}" />
|
|
2059
|
-
</intent-filter>`).join(`
|
|
2060
|
-
`);
|
|
2061
|
-
}
|
|
2062
|
-
function resolveRuntimeDir2(override) {
|
|
2063
|
-
if (override === null)
|
|
2064
|
-
return null;
|
|
2065
|
-
const dir = override ?? process.env.CRAFT_ANDROID_RUNTIME;
|
|
2066
|
-
if (!dir)
|
|
2067
|
-
return null;
|
|
2068
|
-
if (!existsSync5(dir)) {
|
|
2069
|
-
const source = override === undefined ? "CRAFT_ANDROID_RUNTIME points at" : "runtimeDir is";
|
|
2070
|
-
throw new Error(`${source} ${dir}, which does not exist.`);
|
|
2071
|
-
}
|
|
2072
|
-
return dir;
|
|
2073
|
-
}
|
|
2074
|
-
function installRuntime2(output, runtimeDir) {
|
|
2075
|
-
const found = RUNTIME_ABIS.map((abi) => ({ abi, source: join3(runtimeDir, abi, "libcraft.so") })).filter((entry) => existsSync5(entry.source));
|
|
2076
|
-
if (found.length === 0) {
|
|
2077
|
-
throw new Error(`${runtimeDir} has no <abi>/libcraft.so for any of ${RUNTIME_ABIS.join(", ")}. ` + "Run `zig build build-android-all -Doptimize=ReleaseSafe` in packages/zig and point at its zig-out/android.");
|
|
2078
|
-
}
|
|
2079
|
-
if (found.length < RUNTIME_ABIS.length) {
|
|
2080
|
-
const missing = RUNTIME_ABIS.filter((abi) => !found.some((entry) => entry.abi === abi));
|
|
2081
|
-
console.warn(` \u26A0 only ${found.map((entry) => entry.abi).join(", ")} was found; ${missing.join(", ")} is missing. ` + "The app will fall back to the Kotlin shim on those devices.");
|
|
2082
|
-
}
|
|
2083
|
-
const dest = join3(output, "app/src/main/jniLibs");
|
|
2084
|
-
rmSync3(dest, { force: true, recursive: true });
|
|
2085
|
-
for (const { abi, source } of found) {
|
|
2086
|
-
const abiDir = join3(dest, abi);
|
|
2087
|
-
mkdirSync3(abiDir, { recursive: true });
|
|
2088
|
-
cpSync3(source, join3(abiDir, "libcraft.so"));
|
|
2089
|
-
}
|
|
2090
|
-
return true;
|
|
2091
|
-
}
|
|
2092
|
-
async function init2(options) {
|
|
2093
|
-
const { name, packageName, output } = options;
|
|
2094
|
-
console.log(`
|
|
2095
|
-
\u26A1 Initializing Craft Android project: ${name}`);
|
|
2096
|
-
console.log(` Output: ${output}
|
|
2097
|
-
`);
|
|
2098
|
-
const finalPackageName = packageName || `com.craft.${generatedPackageSegment(name)}`;
|
|
2099
|
-
const packagePath = finalPackageName.replace(/\./g, "/");
|
|
2100
|
-
const given = Object.fromEntries(Object.entries(options.config ?? {}).filter(([, value]) => value !== undefined));
|
|
2101
|
-
const config = {
|
|
2102
|
-
...DEFAULT_CONFIG2,
|
|
2103
|
-
...given,
|
|
2104
|
-
appName: name,
|
|
2105
|
-
packageName: finalPackageName
|
|
2106
|
-
};
|
|
2107
|
-
if (config.enableBackgroundLocation)
|
|
2108
|
-
config.enableGeolocation = true;
|
|
2109
|
-
if (config.enableHealthConnect) {
|
|
2110
|
-
config.compileSdk = Math.max(config.compileSdk ?? 36, 36);
|
|
2111
|
-
}
|
|
2112
|
-
normalizeAndroidNetworkConfig(config);
|
|
2113
|
-
validateAndroidConfig(config);
|
|
2114
|
-
if (config.enablePushNotifications && !config.googleServicesFile) {
|
|
2115
|
-
throw new Error("Android push notifications require a googleServicesFile");
|
|
2116
|
-
}
|
|
2117
|
-
if (config.googleServicesFile)
|
|
2118
|
-
validateGoogleServicesFile(config.googleServicesFile, finalPackageName);
|
|
2119
|
-
if (config.appIconPath)
|
|
2120
|
-
requireRegularFile(config.appIconPath, "App icon");
|
|
2121
|
-
const appIconExtension = config.appIconPath ? extname(config.appIconPath).toLowerCase() : undefined;
|
|
2122
|
-
if (config.appIconPath && ![".gif", ".jpg", ".png", ".webp"].includes(appIconExtension ?? "")) {
|
|
2123
|
-
throw new Error(`Unsupported Android app icon format: ${appIconExtension || "(none)"}`);
|
|
2124
|
-
}
|
|
2125
|
-
const dirs = [
|
|
2126
|
-
output,
|
|
2127
|
-
join3(output, "app/src/main/java", packagePath),
|
|
2128
|
-
join3(output, "app/src/main/res/layout"),
|
|
2129
|
-
join3(output, "app/src/main/res/values"),
|
|
2130
|
-
join3(output, "app/src/main/res/drawable"),
|
|
2131
|
-
join3(output, "app/src/main/assets"),
|
|
2132
|
-
join3(output, "gradle/wrapper")
|
|
2133
|
-
];
|
|
2134
|
-
if (existsSync5(output) && !statSync2(output).isDirectory()) {
|
|
2135
|
-
throw new Error(`Android project output must be a directory: ${output}`);
|
|
2136
|
-
}
|
|
2137
|
-
for (const dir of dirs) {
|
|
2138
|
-
if (!existsSync5(dir)) {
|
|
2139
|
-
mkdirSync3(dir, { recursive: true });
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
writeAndroidConfig(output, config);
|
|
2143
|
-
const hasGoogleServices = Boolean(config.googleServicesFile);
|
|
2144
|
-
if (config.googleServicesFile) {
|
|
2145
|
-
cpSync3(config.googleServicesFile, join3(output, "app/google-services.json"));
|
|
2146
|
-
}
|
|
2147
|
-
const mainActivityTemplate = readFileSync4(join3(TEMPLATES_DIR2, "MainActivity.kt.template"), "utf-8");
|
|
2148
|
-
const mainActivity = mainActivityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
|
|
2149
|
-
writeFileSync4(join3(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
|
|
2150
|
-
const craftBridgeTemplate = readFileSync4(join3(TEMPLATES_DIR2, "CraftBridge.kt.template"), "utf-8");
|
|
2151
|
-
const craftBridge = craftBridgeTemplate.replace(/\{\{PROMISE_RUNTIME\}\}/g, () => renderAndroidPromiseRuntime(" ")).replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{ENABLE_SPEECH\}\}/g, String(Boolean(config.enableSpeechRecognition))).replace(/\{\{ENABLE_HAPTICS\}\}/g, String(Boolean(config.enableHaptics))).replace(/\{\{ENABLE_SHARE\}\}/g, String(Boolean(config.enableShare))).replace(/\{\{ENABLE_CAMERA\}\}/g, String(Boolean(config.enableCamera))).replace(/\{\{ENABLE_BIOMETRIC\}\}/g, String(Boolean(config.enableBiometric))).replace(/\{\{ENABLE_PUSH\}\}/g, String(Boolean(config.enablePushNotifications))).replace(/\{\{ENABLE_SECURE_STORAGE\}\}/g, String(Boolean(config.enableSecureStorage))).replace(/\{\{ENABLE_GEOLOCATION\}\}/g, String(Boolean(config.enableGeolocation))).replace(/\{\{ENABLE_BACKGROUND_LOCATION\}\}/g, String(Boolean(config.enableBackgroundLocation))).replace(/\{\{ENABLE_KEEP_AWAKE\}\}/g, String(Boolean(config.enableKeepAwake))).replace(/\{\{ENABLE_DEEP_LINKS\}\}/g, String(Boolean(config.enableDeepLinks))).replace(/\{\{ENABLE_HEALTH_CONNECT\}\}/g, String(Boolean(config.enableHealthConnect))).replace(/\{\{FIREBASE_IMPORT\}\}/g, config.enablePushNotifications ? "import com.google.firebase.messaging.FirebaseMessaging" : "").replace(/\{\{REGISTER_PUSH_IMPLEMENTATION\}\}/g, config.enablePushNotifications ? `activity.runOnUiThread {
|
|
2152
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
|
2153
|
-
&& ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
2154
|
-
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 4204)
|
|
2155
|
-
}
|
|
2156
|
-
try {
|
|
2157
|
-
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
|
|
2158
|
-
val token = if (task.isSuccessful) task.result else null
|
|
2159
|
-
val callback = if (token.isNullOrBlank()) "window._craftPushReject" else "window._craftPushResolve"
|
|
2160
|
-
val payload = JSONObject.quote(token ?: task.exception?.message ?: "Firebase Cloud Messaging is not configured")
|
|
2161
|
-
evaluatePromiseJavascript("$callback && $callback($payload)")
|
|
2162
|
-
}
|
|
2163
|
-
} catch (error: Exception) {
|
|
2164
|
-
val payload = JSONObject.quote(error.message ?: "Firebase Cloud Messaging is not configured")
|
|
2165
|
-
evaluatePromiseJavascript("window._craftPushReject && window._craftPushReject($payload)")
|
|
2166
|
-
}
|
|
2167
|
-
}` : `evaluatePromiseJavascript(
|
|
2168
|
-
"window._craftPushReject && window._craftPushReject('Push notifications are disabled')"
|
|
2169
|
-
)`);
|
|
2170
|
-
writeFileSync4(join3(output, "app/src/main/java", packagePath, "CraftBridge.kt"), craftBridge);
|
|
2171
|
-
const craftNative = readFileSync4(join3(TEMPLATES_DIR2, "CraftNative.kt.template"), "utf-8");
|
|
2172
|
-
const nativeDir = join3(output, "app/src/main/java/com/craft/runtime");
|
|
2173
|
-
mkdirSync3(nativeDir, { recursive: true });
|
|
2174
|
-
writeFileSync4(join3(nativeDir, "CraftNative.kt"), craftNative);
|
|
2175
|
-
const serviceTemplate = readFileSync4(join3(TEMPLATES_DIR2, "LocationRecordingService.kt.template"), "utf-8");
|
|
2176
|
-
writeFileSync4(join3(nativeDir, "LocationRecordingService.kt"), serviceTemplate);
|
|
2177
|
-
const healthTemplate = readFileSync4(join3(TEMPLATES_DIR2, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
|
|
2178
|
-
writeFileSync4(join3(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
2179
|
-
const manifestTemplate = readFileSync4(join3(TEMPLATES_DIR2, "AndroidManifest.xml.template"), "utf-8");
|
|
2180
|
-
const manifest = manifestTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{PERMISSIONS\}\}/g, renderAndroidPermissions(config)).replace(/\{\{USES_CLEARTEXT\}\}/g, config.devServerURL?.startsWith("http://") ? "true" : "false").replace(/\{\{DEEP_LINK_INTENT_FILTERS\}\}/g, renderAndroidDeepLinks(config)).replace(/\{\{BACKGROUND_SERVICE\}\}/g, config.enableBackgroundLocation ? ' <service android:name="com.craft.runtime.LocationRecordingService" android:exported="false" android:foregroundServiceType="location" android:stopWithTask="false" />' : "").replace(/\{\{HEALTH_CONNECT_QUERIES\}\}/g, config.enableHealthConnect ? ` <queries>
|
|
2181
|
-
<package android:name="com.google.android.apps.healthdata" />
|
|
2182
|
-
</queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
|
|
2183
|
-
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
|
|
2184
|
-
</intent-filter>
|
|
2185
|
-
<intent-filter>
|
|
2186
|
-
<action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
|
|
2187
|
-
<category android:name="android.intent.category.HEALTH_PERMISSIONS" />
|
|
2188
|
-
</intent-filter>` : "");
|
|
2189
|
-
writeFileSync4(join3(output, "app/src/main/AndroidManifest.xml"), manifest);
|
|
2190
|
-
const projectGradleTemplate = readFileSync4(join3(TEMPLATES_DIR2, "build.gradle.kts.project.template"), "utf-8");
|
|
2191
|
-
writeFileSync4(join3(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
|
|
2192
|
-
const appGradleTemplate = readFileSync4(join3(TEMPLATES_DIR2, "build.gradle.kts.app.template"), "utf-8");
|
|
2193
|
-
const appGradle = appGradleTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{VERSION_NAME\}\}/g, escapeKotlinString(config.version || "1.0.0")).replace(/\{\{VERSION_CODE\}\}/g, String(config.versionCode || 1)).replace(/\{\{MIN_SDK\}\}/g, String(config.minSdk ?? ANDROID_MIN_SDK)).replace(/\{\{COMPILE_SDK\}\}/g, String(config.compileSdk || 36)).replace(/\{\{TARGET_SDK\}\}/g, String(config.targetSdk || 35)).replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services")' : "").replace(/\{\{FIREBASE_MESSAGING_DEPENDENCY\}\}/g, config.enablePushNotifications ? ' implementation("com.google.firebase:firebase-messaging:24.1.0")' : "").replace(/\{\{HEALTH_CONNECT_DEPENDENCIES\}\}/g, config.enableHealthConnect ? ` implementation("androidx.health.connect:connect-client:1.1.0")
|
|
2194
|
-
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")` : "");
|
|
2195
|
-
writeFileSync4(join3(output, "app/build.gradle.kts"), appGradle);
|
|
2196
|
-
const proguardTemplate = readFileSync4(join3(TEMPLATES_DIR2, "proguard-rules.pro.template"), "utf-8");
|
|
2197
|
-
writeFileSync4(join3(output, "app/proguard-rules.pro"), proguardTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
2198
|
-
const settingsTemplate = readFileSync4(join3(TEMPLATES_DIR2, "settings.gradle.kts.template"), "utf-8");
|
|
2199
|
-
const settings2 = settingsTemplate.replace(/\{\{APP_NAME\}\}/g, escapeKotlinString(generatedGradleProjectName(name)));
|
|
2200
|
-
writeFileSync4(join3(output, "settings.gradle.kts"), settings2);
|
|
2201
|
-
const gradleProperties = `org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|
2202
|
-
android.useAndroidX=true
|
|
2203
|
-
kotlin.code.style=official
|
|
2204
|
-
android.nonTransitiveRClass=true
|
|
2205
|
-
`;
|
|
2206
|
-
writeFileSync4(join3(output, "gradle.properties"), gradleProperties);
|
|
2207
|
-
writeFileSync4(join3(output, "local.properties"), `# SDK location will be set by Android Studio
|
|
2208
|
-
`);
|
|
2209
|
-
const gradleWrapperProps = `distributionBase=GRADLE_USER_HOME
|
|
2210
|
-
distributionPath=wrapper/dists
|
|
2211
|
-
distributionUrl=https\\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
|
2212
|
-
networkTimeout=10000
|
|
2213
|
-
validateDistributionUrl=true
|
|
2214
|
-
zipStoreBase=GRADLE_USER_HOME
|
|
2215
|
-
zipStorePath=wrapper/dists
|
|
2216
|
-
`;
|
|
2217
|
-
writeFileSync4(join3(output, "gradle/wrapper/gradle-wrapper.properties"), gradleWrapperProps);
|
|
2218
|
-
const stringsXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
2219
|
-
<resources>
|
|
2220
|
-
<string name="app_name">${escapeXml(name)}</string>
|
|
2221
|
-
</resources>
|
|
2222
|
-
`;
|
|
2223
|
-
writeFileSync4(join3(output, "app/src/main/res/values/strings.xml"), stringsXml);
|
|
2224
|
-
const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
2225
|
-
<resources>
|
|
2226
|
-
<color name="primary">#1a1a2e</color>
|
|
2227
|
-
<color name="primary_dark">#16162b</color>
|
|
2228
|
-
<color name="accent">#4ade80</color>
|
|
2229
|
-
<color name="background">${config.backgroundColor}</color>
|
|
2230
|
-
</resources>
|
|
2231
|
-
`;
|
|
2232
|
-
writeFileSync4(join3(output, "app/src/main/res/values/colors.xml"), colorsXml);
|
|
2233
|
-
const appIconXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
2234
|
-
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2235
|
-
android:width="108dp"
|
|
2236
|
-
android:height="108dp"
|
|
2237
|
-
android:viewportWidth="108"
|
|
2238
|
-
android:viewportHeight="108">
|
|
2239
|
-
<path android:fillColor="${config.backgroundColor}" android:pathData="M0,0h108v108h-108z" />
|
|
2240
|
-
<path android:fillColor="#10B981" android:pathData="M18,78L42,42l12,18l12,-12l24,30z" />
|
|
2241
|
-
</vector>
|
|
2242
|
-
`;
|
|
2243
|
-
if (config.appIconPath) {
|
|
2244
|
-
cpSync3(config.appIconPath, join3(output, `app/src/main/res/drawable/craft_app_icon${appIconExtension}`));
|
|
2245
|
-
} else {
|
|
2246
|
-
writeFileSync4(join3(output, "app/src/main/res/drawable/craft_app_icon.xml"), appIconXml);
|
|
2247
|
-
}
|
|
2248
|
-
const themesXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
2249
|
-
<resources>
|
|
2250
|
-
<style name="Theme.CraftApp" parent="Theme.Material3.DayNight.NoActionBar">
|
|
2251
|
-
<item name="android:statusBarColor">@color/primary_dark</item>
|
|
2252
|
-
<item name="android:navigationBarColor">@color/primary</item>
|
|
2253
|
-
<item name="android:windowBackground">@color/background</item>
|
|
2254
|
-
</style>
|
|
2255
|
-
</resources>
|
|
2256
|
-
`;
|
|
2257
|
-
writeFileSync4(join3(output, "app/src/main/res/values/themes.xml"), themesXml);
|
|
2258
|
-
const activityMainXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
2259
|
-
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
2260
|
-
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2261
|
-
android:layout_width="match_parent"
|
|
2262
|
-
android:layout_height="match_parent"
|
|
2263
|
-
android:fitsSystemWindows="true">
|
|
2264
|
-
|
|
2265
|
-
<WebView
|
|
2266
|
-
android:id="@+id/webview"
|
|
2267
|
-
android:layout_width="match_parent"
|
|
2268
|
-
android:layout_height="match_parent" />
|
|
2269
|
-
|
|
2270
|
-
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
2271
|
-
`;
|
|
2272
|
-
writeFileSync4(join3(output, "app/src/main/res/layout/activity_main.xml"), activityMainXml);
|
|
2273
|
-
const placeholderHtml = `<!DOCTYPE html>
|
|
2274
|
-
<html>
|
|
2275
|
-
<head>
|
|
2276
|
-
<meta charset="UTF-8">
|
|
2277
|
-
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
2278
|
-
<title>${escapeXml(name)}</title>
|
|
2279
|
-
<style>
|
|
2280
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2281
|
-
body {
|
|
2282
|
-
font-family: 'Roboto', sans-serif;
|
|
2283
|
-
background: ${config.backgroundColor};
|
|
2284
|
-
color: white;
|
|
2285
|
-
min-height: 100vh;
|
|
2286
|
-
display: flex;
|
|
2287
|
-
justify-content: center;
|
|
2288
|
-
align-items: center;
|
|
2289
|
-
}
|
|
2290
|
-
.container { text-align: center; padding: 2rem; }
|
|
2291
|
-
h1 { font-size: 2.5rem; margin-bottom: 1rem; }
|
|
2292
|
-
p { opacity: 0.7; }
|
|
2293
|
-
.ready { color: #4ade80; font-size: 0.9rem; margin-top: 2rem; }
|
|
2294
|
-
</style>
|
|
2295
|
-
</head>
|
|
2296
|
-
<body>
|
|
2297
|
-
<div class="container">
|
|
2298
|
-
<h1>\u26A1 ${escapeXml(name)}</h1>
|
|
2299
|
-
<p>Built with Craft Android</p>
|
|
2300
|
-
<p class="ready" id="status">Waiting for Craft bridge...</p>
|
|
2301
|
-
</div>
|
|
2302
|
-
<script>
|
|
2303
|
-
window.addEventListener('craftReady', (e) => {
|
|
2304
|
-
document.getElementById('status').textContent = \`\u2713 Craft bridge ready (platform: \${e.detail.platform})\`;
|
|
2305
|
-
console.log('Craft capabilities:', e.detail.capabilities);
|
|
2306
|
-
});
|
|
2307
|
-
</script>
|
|
2308
|
-
</body>
|
|
2309
|
-
</html>`;
|
|
2310
|
-
writeFileSync4(join3(output, "app/src/main/assets/index.html"), placeholderHtml);
|
|
2311
|
-
const runtimeDir = resolveRuntimeDir2(options.runtimeDir);
|
|
2312
|
-
if (runtimeDir) {
|
|
2313
|
-
installRuntime2(output, runtimeDir);
|
|
2314
|
-
console.log(" Installed the Zig runtime from", runtimeDir);
|
|
2315
|
-
}
|
|
2316
|
-
console.log("\u2705 Project initialized");
|
|
2317
|
-
console.log("");
|
|
2318
|
-
console.log("Next steps:");
|
|
2319
|
-
console.log(` 1. cd ${output}`);
|
|
2320
|
-
console.log(" 2. Add your web content to app/src/main/assets/index.html");
|
|
2321
|
-
console.log(" 3. Run: craft android build");
|
|
2322
|
-
console.log(" 4. Run: craft android open");
|
|
2323
|
-
console.log("");
|
|
2324
|
-
}
|
|
2325
|
-
async function build2(options) {
|
|
2326
|
-
const { htmlPath, devServer, output, release, compile = true } = options;
|
|
2327
|
-
console.log(`
|
|
2328
|
-
\uD83D\uDCE6 Building Craft Android project...`);
|
|
2329
|
-
const configPath = join3(output, "craft.config.json");
|
|
2330
|
-
if (!existsSync5(configPath)) {
|
|
2331
|
-
throw new Error(`No craft.config.json found in ${output}. Run 'craft android init' first.`);
|
|
2332
|
-
}
|
|
2333
|
-
const config = JSON.parse(readFileSync4(configPath, "utf-8"));
|
|
2334
|
-
if (existsSync5(join3(output, "app/src/main/jniLibs"))) {
|
|
2335
|
-
const runtimeDir = resolveRuntimeDir2(options.runtimeDir);
|
|
2336
|
-
if (runtimeDir) {
|
|
2337
|
-
installRuntime2(output, runtimeDir);
|
|
2338
|
-
console.log(" Refreshed the Zig runtime from", runtimeDir);
|
|
2339
|
-
} else {
|
|
2340
|
-
console.log(" Keeping the Zig runtime installed at init (no runtime directory configured)");
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
if (devServer) {
|
|
2344
|
-
const url = androidWebUrl(devServer, "Android dev server URL");
|
|
2345
|
-
config.devServerURL = url.toString();
|
|
2346
|
-
config.trustedOrigins = [...new Set([
|
|
2347
|
-
...(config.trustedOrigins ?? []).map((value) => androidWebUrl(value, "Android trusted origin").origin),
|
|
2348
|
-
url.origin
|
|
2349
|
-
])];
|
|
2350
|
-
writeAndroidConfig(output, config);
|
|
2351
|
-
console.log(` Dev server: ${devServer}`);
|
|
2352
|
-
}
|
|
2353
|
-
if (htmlPath) {
|
|
2354
|
-
syncAndroidWebAssets(htmlPath, output);
|
|
2355
|
-
config.hasBundledFallback = Boolean(devServer);
|
|
2356
|
-
writeAndroidConfig(output, config);
|
|
2357
|
-
console.log(` Synced: ${htmlPath} \u2192 assets/`);
|
|
2358
|
-
}
|
|
2359
|
-
if (!compile)
|
|
2360
|
-
return;
|
|
2361
|
-
const gradlewPath = join3(output, "gradlew");
|
|
2362
|
-
if (!existsSync5(gradlewPath)) {
|
|
2363
|
-
console.log(" Generating Gradle wrapper...");
|
|
2364
|
-
try {
|
|
2365
|
-
await $2`cd ${output} && gradle wrapper`.quiet();
|
|
2366
|
-
} catch (error) {
|
|
2367
|
-
throw new Error("Gradle is unavailable. Install Android Studio or Gradle before building Android.", { cause: error });
|
|
2368
|
-
}
|
|
2369
|
-
}
|
|
2370
|
-
const buildType = release ? "assembleRelease" : "assembleDebug";
|
|
2371
|
-
console.log(` Building ${release ? "release" : "debug"} APK...`);
|
|
2372
|
-
try {
|
|
2373
|
-
await $2`cd ${output} && ./gradlew ${buildType}`;
|
|
2374
|
-
const apkPath = release ? "app/build/outputs/apk/release/app-release.apk" : "app/build/outputs/apk/debug/app-debug.apk";
|
|
2375
|
-
console.log(`\u2705 APK built: ${apkPath}`);
|
|
2376
|
-
} catch (error) {
|
|
2377
|
-
console.error("Build failed. Open in Android Studio for details.");
|
|
2378
|
-
throw error;
|
|
2379
|
-
}
|
|
2380
|
-
console.log("");
|
|
2381
|
-
}
|
|
2382
|
-
async function open2(options) {
|
|
2383
|
-
const { output } = options;
|
|
2384
|
-
if (!existsSync5(output)) {
|
|
2385
|
-
throw new Error(`No Android project found in ${output}. Run 'craft android init' first.`);
|
|
2386
|
-
}
|
|
2387
|
-
console.log(`\uD83D\uDE80 Opening Android project in Android Studio...`);
|
|
2388
|
-
try {
|
|
2389
|
-
await $2`open -a "Android Studio" ${output}`.quiet();
|
|
2390
|
-
} catch {
|
|
2391
|
-
try {
|
|
2392
|
-
await $2`studio ${output}`.quiet();
|
|
2393
|
-
} catch {
|
|
2394
|
-
console.log("\u26A0\uFE0F Android Studio not found.");
|
|
2395
|
-
console.log(" Please open the project manually in Android Studio:");
|
|
2396
|
-
console.log(` ${output}`);
|
|
2397
|
-
}
|
|
2398
|
-
}
|
|
2399
|
-
}
|
|
2400
|
-
async function run2(options) {
|
|
2401
|
-
const { device, output } = options;
|
|
2402
|
-
await build2({ output });
|
|
2403
|
-
console.log("\uD83D\uDCF1 Installing and running on device...");
|
|
2404
|
-
try {
|
|
2405
|
-
const apkPath = join3(output, "app/build/outputs/apk/debug/app-debug.apk");
|
|
2406
|
-
if (device) {
|
|
2407
|
-
await $2`adb -s ${device} install -r ${apkPath}`;
|
|
2408
|
-
} else {
|
|
2409
|
-
await $2`adb install -r ${apkPath}`;
|
|
2410
|
-
}
|
|
2411
|
-
const config = JSON.parse(readFileSync4(join3(output, "craft.config.json"), "utf-8"));
|
|
2412
|
-
const launchCmd = `${config.packageName}/.MainActivity`;
|
|
2413
|
-
if (device) {
|
|
2414
|
-
await $2`adb -s ${device} shell am start -n ${launchCmd}`;
|
|
2415
|
-
} else {
|
|
2416
|
-
await $2`adb shell am start -n ${launchCmd}`;
|
|
2417
|
-
}
|
|
2418
|
-
console.log("\u2705 App launched");
|
|
2419
|
-
} catch (error) {
|
|
2420
|
-
console.error("Failed to install/run. Make sure:");
|
|
2421
|
-
console.error(" 1. ADB is installed and in PATH");
|
|
2422
|
-
console.error(" 2. A device/emulator is connected (run: adb devices)");
|
|
2423
|
-
throw error;
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
var $2, ANDROID_PROMISE_RUNTIME = `
|
|
2427
|
-
if (window.__craftRejectPendingPromises) {
|
|
2428
|
-
window.__craftRejectPendingPromises('Android bridge reinitialized');
|
|
2429
|
-
}
|
|
2430
|
-
if (window.__craftRejectPermissionRequests) {
|
|
2431
|
-
window.__craftRejectPermissionRequests('Android bridge reinitialized');
|
|
2432
|
-
}
|
|
2433
|
-
window.__craftPromiseRuntimeClosed = false;
|
|
2434
|
-
window.__craftPendingPromises = Object.create(null);
|
|
2435
|
-
window.__craftPromise = function(channel, resolveName, rejectName, invoke, timeoutMs, timeoutError) {
|
|
2436
|
-
if (window.__craftPromiseRuntimeClosed) {
|
|
2437
|
-
return Promise.reject(new Error('Android bridge is closed'));
|
|
2438
|
-
}
|
|
2439
|
-
if (window.__craftPendingPromises[channel]) {
|
|
2440
|
-
return Promise.reject(new Error('A '.concat(channel, ' request is already in progress')));
|
|
2441
|
-
}
|
|
2442
|
-
|
|
2443
|
-
return new Promise(function(resolve, reject) {
|
|
2444
|
-
var entry = {settled: false, timer: null, settle: null};
|
|
2445
|
-
var resolveCallback;
|
|
2446
|
-
var rejectCallback;
|
|
2447
|
-
|
|
2448
|
-
entry.settle = function(succeeded, value) {
|
|
2449
|
-
if (entry.settled || window.__craftPendingPromises[channel] !== entry) return;
|
|
2450
|
-
entry.settled = true;
|
|
2451
|
-
if (entry.timer !== null) clearTimeout(entry.timer);
|
|
2452
|
-
if (window[resolveName] === resolveCallback) window[resolveName] = null;
|
|
2453
|
-
if (window[rejectName] === rejectCallback) window[rejectName] = null;
|
|
2454
|
-
delete window.__craftPendingPromises[channel];
|
|
2455
|
-
if (succeeded) resolve(value);
|
|
2456
|
-
else reject(value);
|
|
2457
|
-
};
|
|
2458
|
-
|
|
2459
|
-
resolveCallback = function(value) {
|
|
2460
|
-
entry.settle(true, value);
|
|
2461
|
-
};
|
|
2462
|
-
rejectCallback = function(error) {
|
|
2463
|
-
entry.settle(false, error);
|
|
2464
|
-
};
|
|
2465
|
-
window[resolveName] = resolveCallback;
|
|
2466
|
-
window[rejectName] = rejectCallback;
|
|
2467
|
-
window.__craftPendingPromises[channel] = entry;
|
|
2468
|
-
|
|
2469
|
-
if (timeoutMs > 0) {
|
|
2470
|
-
entry.timer = setTimeout(function() {
|
|
2471
|
-
entry.settle(false, timeoutError || new Error(channel.concat(' request timed out')));
|
|
2472
|
-
}, timeoutMs);
|
|
2473
|
-
}
|
|
2474
|
-
|
|
2475
|
-
try {
|
|
2476
|
-
invoke();
|
|
2477
|
-
}
|
|
2478
|
-
catch (error) {
|
|
2479
|
-
entry.settle(false, error);
|
|
2480
|
-
}
|
|
2481
|
-
});
|
|
2482
|
-
};
|
|
2483
|
-
|
|
2484
|
-
window.__craftRejectPendingPromises = function(message) {
|
|
2485
|
-
window.__craftPromiseRuntimeClosed = true;
|
|
2486
|
-
Object.keys(window.__craftPendingPromises).forEach(function(channel) {
|
|
2487
|
-
var entry = window.__craftPendingPromises[channel];
|
|
2488
|
-
if (entry) entry.settle(false, new Error(message || 'Android bridge closed'));
|
|
2489
|
-
});
|
|
2490
|
-
};
|
|
2491
|
-
`, TEMPLATES_DIR2, LOCAL_DEVELOPMENT_HOSTS, KOTLIN_KEYWORDS, ANDROID_MIN_SDK = 26, DEFAULT_CONFIG2, RUNTIME_ABIS;
|
|
2492
|
-
var init_src2 = __esm(() => {
|
|
2493
|
-
({ $: $2 } = globalThis.Bun);
|
|
2494
|
-
TEMPLATES_DIR2 = join3(dirname2(import.meta.dir), "templates");
|
|
2495
|
-
LOCAL_DEVELOPMENT_HOSTS = new Set(["localhost", "127.0.0.1", "10.0.2.2"]);
|
|
2496
|
-
KOTLIN_KEYWORDS = new Set([
|
|
2497
|
-
"as",
|
|
2498
|
-
"break",
|
|
2499
|
-
"class",
|
|
2500
|
-
"continue",
|
|
2501
|
-
"do",
|
|
2502
|
-
"else",
|
|
2503
|
-
"false",
|
|
2504
|
-
"for",
|
|
2505
|
-
"fun",
|
|
2506
|
-
"if",
|
|
2507
|
-
"in",
|
|
2508
|
-
"interface",
|
|
2509
|
-
"is",
|
|
2510
|
-
"null",
|
|
2511
|
-
"object",
|
|
2512
|
-
"package",
|
|
2513
|
-
"return",
|
|
2514
|
-
"super",
|
|
2515
|
-
"this",
|
|
2516
|
-
"throw",
|
|
2517
|
-
"true",
|
|
2518
|
-
"try",
|
|
2519
|
-
"typealias",
|
|
2520
|
-
"typeof",
|
|
2521
|
-
"val",
|
|
2522
|
-
"var",
|
|
2523
|
-
"when",
|
|
2524
|
-
"while"
|
|
2525
|
-
]);
|
|
2526
|
-
DEFAULT_CONFIG2 = {
|
|
2527
|
-
version: "1.0.0",
|
|
2528
|
-
versionCode: 1,
|
|
2529
|
-
darkMode: true,
|
|
2530
|
-
backgroundColor: "#0b1712",
|
|
2531
|
-
enableSpeechRecognition: false,
|
|
2532
|
-
enableHaptics: false,
|
|
2533
|
-
enableShare: false,
|
|
2534
|
-
enableCamera: false,
|
|
2535
|
-
enableBiometric: false,
|
|
2536
|
-
enablePushNotifications: false,
|
|
2537
|
-
enableSecureStorage: false,
|
|
2538
|
-
enableGeolocation: false,
|
|
2539
|
-
enableBackgroundLocation: false,
|
|
2540
|
-
enableHealthConnect: false,
|
|
2541
|
-
enableKeepAwake: false,
|
|
2542
|
-
enableDeepLinks: false,
|
|
2543
|
-
urlSchemes: [],
|
|
2544
|
-
trustedOrigins: [],
|
|
2545
|
-
minSdk: ANDROID_MIN_SDK,
|
|
2546
|
-
compileSdk: 36,
|
|
2547
|
-
targetSdk: 35
|
|
2548
|
-
};
|
|
2549
|
-
RUNTIME_ABIS = ["arm64-v8a", "x86_64"];
|
|
2550
|
-
});
|
|
2551
|
-
|
|
2552
1142
|
// ../../node_modules/@stacksjs/clapp/dist/index.js
|
|
2553
1143
|
import { existsSync } from "fs";
|
|
2554
1144
|
import fs from "fs/promises";
|
|
@@ -4633,7 +3223,7 @@ var prefix = getPrefix();
|
|
|
4633
3223
|
|
|
4634
3224
|
// bin/cli.ts
|
|
4635
3225
|
import { spawn as spawn2 } from "child_process";
|
|
4636
|
-
import { existsSync as
|
|
3226
|
+
import { existsSync as existsSync4 } from "fs";
|
|
4637
3227
|
import process6 from "process";
|
|
4638
3228
|
|
|
4639
3229
|
// src/binary-resolver.ts
|
|
@@ -4713,7 +3303,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
4713
3303
|
`);
|
|
4714
3304
|
}
|
|
4715
3305
|
// package.json
|
|
4716
|
-
var version = "0.0.
|
|
3306
|
+
var version = "0.0.107";
|
|
4717
3307
|
|
|
4718
3308
|
// bin/cli.ts
|
|
4719
3309
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
|
@@ -4722,16 +3312,18 @@ if (spawnedFrom) {
|
|
|
4722
3312
|
process6.exit(1);
|
|
4723
3313
|
}
|
|
4724
3314
|
var cli = new CLI("craft");
|
|
3315
|
+
var iosModuleURL = new URL("../dist/ios/src/index.js", import.meta.url).href;
|
|
3316
|
+
var androidModuleURL = new URL("../dist/android/src/index.js", import.meta.url).href;
|
|
4725
3317
|
async function runCraftBinary(args) {
|
|
4726
3318
|
const craftPath = resolveCraftBinary();
|
|
4727
|
-
return new Promise((
|
|
3319
|
+
return new Promise((resolve, reject) => {
|
|
4728
3320
|
const proc = spawn2(craftPath, args, {
|
|
4729
3321
|
stdio: "inherit",
|
|
4730
3322
|
env: { ...process6.env, [CRAFT_CLI_SPAWN_MARKER]: craftPath }
|
|
4731
3323
|
});
|
|
4732
3324
|
proc.on("exit", (code) => {
|
|
4733
3325
|
if (code === 0 || code === null)
|
|
4734
|
-
|
|
3326
|
+
resolve();
|
|
4735
3327
|
else
|
|
4736
3328
|
reject(new Error(`Craft exited with code ${code}`));
|
|
4737
3329
|
});
|
|
@@ -4825,7 +3417,7 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
|
|
|
4825
3417
|
console.log(`Platforms: ${platforms.join(", ")}`);
|
|
4826
3418
|
console.log(`Mode: ${options?.release ? "Release" : "Debug"}
|
|
4827
3419
|
`);
|
|
4828
|
-
const { existsSync:
|
|
3420
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
4829
3421
|
const { spawn: spawn3 } = await import("child_process");
|
|
4830
3422
|
for (const platform of platforms) {
|
|
4831
3423
|
console.log(`\uD83D\uDCE6 Building for ${platform}...`);
|
|
@@ -4836,9 +3428,9 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
|
|
|
4836
3428
|
continue;
|
|
4837
3429
|
}
|
|
4838
3430
|
const iosDir = "./ios";
|
|
4839
|
-
if (
|
|
3431
|
+
if (existsSync5(iosDir)) {
|
|
4840
3432
|
const buildType = options?.release ? "Release" : "Debug";
|
|
4841
|
-
await new Promise((
|
|
3433
|
+
await new Promise((resolve, reject) => {
|
|
4842
3434
|
const proc = spawn3("xcodebuild", [
|
|
4843
3435
|
"-project",
|
|
4844
3436
|
`${iosDir}/App.xcodeproj`,
|
|
@@ -4850,7 +3442,7 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
|
|
|
4850
3442
|
"iphoneos",
|
|
4851
3443
|
"build"
|
|
4852
3444
|
], { stdio: "inherit" });
|
|
4853
|
-
proc.on("exit", (code) => code === 0 ?
|
|
3445
|
+
proc.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Exit ${code}`)));
|
|
4854
3446
|
proc.on("error", reject);
|
|
4855
3447
|
});
|
|
4856
3448
|
console.log(` \u2705 iOS ${buildType} build complete`);
|
|
@@ -4859,14 +3451,14 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
|
|
|
4859
3451
|
}
|
|
4860
3452
|
} else if (platform === "android") {
|
|
4861
3453
|
const androidDir = "./android";
|
|
4862
|
-
if (
|
|
3454
|
+
if (existsSync5(androidDir)) {
|
|
4863
3455
|
const task = options?.release ? "assembleRelease" : "assembleDebug";
|
|
4864
|
-
await new Promise((
|
|
3456
|
+
await new Promise((resolve, reject) => {
|
|
4865
3457
|
const proc = spawn3("./gradlew", [task], {
|
|
4866
3458
|
cwd: androidDir,
|
|
4867
3459
|
stdio: "inherit"
|
|
4868
3460
|
});
|
|
4869
|
-
proc.on("exit", (code) => code === 0 ?
|
|
3461
|
+
proc.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Exit ${code}`)));
|
|
4870
3462
|
proc.on("error", reject);
|
|
4871
3463
|
});
|
|
4872
3464
|
console.log(` \u2705 Android ${options?.release ? "Release" : "Debug"} build complete`);
|
|
@@ -4874,9 +3466,9 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
|
|
|
4874
3466
|
console.log(` \u26A0\uFE0F No Android project found. Run: craft android init`);
|
|
4875
3467
|
}
|
|
4876
3468
|
} else if (platform === "macos" || platform === "windows" || platform === "linux") {
|
|
4877
|
-
const zigDir =
|
|
3469
|
+
const zigDir = existsSync5("./packages/zig") ? "./packages/zig" : ".";
|
|
4878
3470
|
const optimizeFlag = options?.release ? "-Doptimize=ReleaseSafe" : "";
|
|
4879
|
-
await new Promise((
|
|
3471
|
+
await new Promise((resolve, reject) => {
|
|
4880
3472
|
const args = ["build"];
|
|
4881
3473
|
if (optimizeFlag)
|
|
4882
3474
|
args.push(optimizeFlag);
|
|
@@ -4884,7 +3476,7 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
|
|
|
4884
3476
|
cwd: zigDir,
|
|
4885
3477
|
stdio: "inherit"
|
|
4886
3478
|
});
|
|
4887
|
-
proc.on("exit", (code) => code === 0 ?
|
|
3479
|
+
proc.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Exit ${code}`)));
|
|
4888
3480
|
proc.on("error", reject);
|
|
4889
3481
|
});
|
|
4890
3482
|
console.log(` \u2705 ${platform} build complete`);
|
|
@@ -4923,7 +3515,7 @@ cli.command("package", "Create installers for your Craft application").option("-
|
|
|
4923
3515
|
const { packageApp: packageApp2 } = packageModule;
|
|
4924
3516
|
let config;
|
|
4925
3517
|
if (options?.config) {
|
|
4926
|
-
if (!
|
|
3518
|
+
if (!existsSync4(options.config)) {
|
|
4927
3519
|
console.error(`\u274C Config file not found: ${options.config}`);
|
|
4928
3520
|
process6.exit(1);
|
|
4929
3521
|
}
|
|
@@ -5009,20 +3601,22 @@ cli.command("package", "Create installers for your Craft application").option("-
|
|
|
5009
3601
|
process6.exit(1);
|
|
5010
3602
|
}
|
|
5011
3603
|
});
|
|
5012
|
-
cli.command("ios init <name>", "Initialize a new iOS project").option("--bundle-id <id>", "Bundle identifier (e.g., com.example.app)").option("--team-id <id>", "Apple Developer Team ID").option("-o, --output <dir>", "Output directory", { default: "./ios" }).example("craft ios init MyApp").example("craft ios init MyApp --bundle-id com.example.myapp").action(async (name, options) => {
|
|
5013
|
-
const iosModule = await
|
|
3604
|
+
cli.command("ios init <name>", "Initialize a new iOS project").option("--bundle-id <id>", "Bundle identifier (e.g., com.example.app)").option("--team-id <id>", "Apple Developer Team ID").option("--renderer <kind>", "iOS renderer: web (default) or native").option("-o, --output <dir>", "Output directory", { default: "./ios" }).example("craft ios init MyApp").example("craft ios init MyApp --bundle-id com.example.myapp").action(async (name, options) => {
|
|
3605
|
+
const iosModule = await import(iosModuleURL);
|
|
5014
3606
|
await iosModule.init({
|
|
5015
3607
|
name,
|
|
5016
3608
|
bundleId: options?.bundleId,
|
|
5017
3609
|
teamId: options?.teamId,
|
|
5018
|
-
output: options?.output || "./ios"
|
|
3610
|
+
output: options?.output || "./ios",
|
|
3611
|
+
config: options?.renderer ? { renderer: options.renderer } : undefined
|
|
5019
3612
|
});
|
|
5020
3613
|
});
|
|
5021
|
-
cli.command("ios build", "Build iOS project").option("--html-path <path>", "Path to HTML file").option("-d, --dev-server <url>", "Development server URL").option("-o, --output <dir>", "iOS project directory", { default: "./ios" }).option("-w, --watch", "Watch for file changes and rebuild").example("craft ios build").example("craft ios build --html-path ./dist/index.html").example("craft ios build --dev-server http://localhost:3456").example("craft ios build --watch").action(async (options) => {
|
|
5022
|
-
const iosModule = await
|
|
3614
|
+
cli.command("ios build", "Build iOS project").option("--html-path <path>", "Path to HTML file").option("--native-bundle <path>", "JavaScript bundle compiled by stx-native").option("-d, --dev-server <url>", "Development server URL").option("-o, --output <dir>", "iOS project directory", { default: "./ios" }).option("-w, --watch", "Watch for file changes and rebuild").example("craft ios build").example("craft ios build --html-path ./dist/index.html").example("craft ios build --native-bundle ./screen.js").example("craft ios build --dev-server http://localhost:3456").example("craft ios build --watch").action(async (options) => {
|
|
3615
|
+
const iosModule = await import(iosModuleURL);
|
|
5023
3616
|
const doBuild = async () => {
|
|
5024
3617
|
await iosModule.build({
|
|
5025
3618
|
htmlPath: options?.htmlPath,
|
|
3619
|
+
nativeBundlePath: options?.nativeBundle,
|
|
5026
3620
|
devServer: options?.devServer,
|
|
5027
3621
|
output: options?.output || "./ios"
|
|
5028
3622
|
});
|
|
@@ -5033,8 +3627,8 @@ cli.command("ios build", "Build iOS project").option("--html-path <path>", "Path
|
|
|
5033
3627
|
\uD83D\uDC40 Watching for changes...
|
|
5034
3628
|
`);
|
|
5035
3629
|
const { watch } = await import("fs");
|
|
5036
|
-
const { dirname
|
|
5037
|
-
const watchPath = options?.htmlPath ?
|
|
3630
|
+
const { dirname } = await import("path");
|
|
3631
|
+
const watchPath = options?.htmlPath ? dirname(options.htmlPath) : ".";
|
|
5038
3632
|
watch(watchPath, { recursive: true }, async (event, filename) => {
|
|
5039
3633
|
if (filename && (filename.endsWith(".html") || filename.endsWith(".js") || filename.endsWith(".css"))) {
|
|
5040
3634
|
console.log(`
|
|
@@ -5046,31 +3640,33 @@ cli.command("ios build", "Build iOS project").option("--html-path <path>", "Path
|
|
|
5046
3640
|
}
|
|
5047
3641
|
});
|
|
5048
3642
|
cli.command("ios open", "Open iOS project in Xcode").option("-o, --output <dir>", "iOS project directory", { default: "./ios" }).action(async (options) => {
|
|
5049
|
-
const iosModule = await
|
|
3643
|
+
const iosModule = await import(iosModuleURL);
|
|
5050
3644
|
await iosModule.open({
|
|
5051
3645
|
output: options?.output || "./ios"
|
|
5052
3646
|
});
|
|
5053
3647
|
});
|
|
5054
3648
|
cli.command("ios run", "Build and run on iOS device or simulator").option("-s, --simulator", "Run on simulator instead of device").option("-o, --output <dir>", "iOS project directory", { default: "./ios" }).example("craft ios run").example("craft ios run --simulator").action(async (options) => {
|
|
5055
|
-
const iosModule = await
|
|
3649
|
+
const iosModule = await import(iosModuleURL);
|
|
5056
3650
|
await iosModule.run({
|
|
5057
3651
|
simulator: options?.simulator || false,
|
|
5058
3652
|
output: options?.output || "./ios"
|
|
5059
3653
|
});
|
|
5060
3654
|
});
|
|
5061
|
-
cli.command("android init <name>", "Initialize a new Android project").option("--package <name>", "Package name (e.g., com.example.app)").option("-o, --output <dir>", "Output directory", { default: "./android" }).example("craft android init MyApp").example("craft android init MyApp --package com.example.myapp").action(async (name, options) => {
|
|
5062
|
-
const androidModule = await
|
|
3655
|
+
cli.command("android init <name>", "Initialize a new Android project").option("--package <name>", "Package name (e.g., com.example.app)").option("--renderer <kind>", "Android renderer: web (default) or native").option("-o, --output <dir>", "Output directory", { default: "./android" }).example("craft android init MyApp").example("craft android init MyApp --package com.example.myapp").action(async (name, options) => {
|
|
3656
|
+
const androidModule = await import(androidModuleURL);
|
|
5063
3657
|
await androidModule.init({
|
|
5064
3658
|
name,
|
|
5065
3659
|
packageName: options?.package,
|
|
5066
|
-
output: options?.output || "./android"
|
|
3660
|
+
output: options?.output || "./android",
|
|
3661
|
+
config: options?.renderer ? { renderer: options.renderer } : undefined
|
|
5067
3662
|
});
|
|
5068
3663
|
});
|
|
5069
|
-
cli.command("android build", "Build Android project").option("--html-path <path>", "Path to HTML file").option("-d, --dev-server <url>", "Development server URL").option("-o, --output <dir>", "Android project directory", { default: "./android" }).option("--release", "Build release APK").option("-w, --watch", "Watch for file changes and rebuild").example("craft android build").example("craft android build --release").example("craft android build --watch").action(async (options) => {
|
|
5070
|
-
const androidModule = await
|
|
3664
|
+
cli.command("android build", "Build Android project").option("--html-path <path>", "Path to HTML file").option("--native-bundle <path>", "JavaScript bundle compiled by stx-native").option("-d, --dev-server <url>", "Development server URL").option("-o, --output <dir>", "Android project directory", { default: "./android" }).option("--release", "Build release APK").option("-w, --watch", "Watch for file changes and rebuild").example("craft android build").example("craft android build --release").example("craft android build --native-bundle ./screen.js").example("craft android build --watch").action(async (options) => {
|
|
3665
|
+
const androidModule = await import(androidModuleURL);
|
|
5071
3666
|
const doBuild = async () => {
|
|
5072
3667
|
await androidModule.build({
|
|
5073
3668
|
htmlPath: options?.htmlPath,
|
|
3669
|
+
nativeBundlePath: options?.nativeBundle,
|
|
5074
3670
|
devServer: options?.devServer,
|
|
5075
3671
|
output: options?.output || "./android",
|
|
5076
3672
|
release: options?.release || false
|
|
@@ -5082,8 +3678,8 @@ cli.command("android build", "Build Android project").option("--html-path <path>
|
|
|
5082
3678
|
\uD83D\uDC40 Watching for changes...
|
|
5083
3679
|
`);
|
|
5084
3680
|
const { watch } = await import("fs");
|
|
5085
|
-
const { dirname
|
|
5086
|
-
const watchPath = options?.htmlPath ?
|
|
3681
|
+
const { dirname } = await import("path");
|
|
3682
|
+
const watchPath = options?.htmlPath ? dirname(options.htmlPath) : ".";
|
|
5087
3683
|
watch(watchPath, { recursive: true }, async (event, filename) => {
|
|
5088
3684
|
if (filename && (filename.endsWith(".html") || filename.endsWith(".js") || filename.endsWith(".css"))) {
|
|
5089
3685
|
console.log(`
|
|
@@ -5095,13 +3691,13 @@ cli.command("android build", "Build Android project").option("--html-path <path>
|
|
|
5095
3691
|
}
|
|
5096
3692
|
});
|
|
5097
3693
|
cli.command("android open", "Open Android project in Android Studio").option("-o, --output <dir>", "Android project directory", { default: "./android" }).action(async (options) => {
|
|
5098
|
-
const androidModule = await
|
|
3694
|
+
const androidModule = await import(androidModuleURL);
|
|
5099
3695
|
await androidModule.open({
|
|
5100
3696
|
output: options?.output || "./android"
|
|
5101
3697
|
});
|
|
5102
3698
|
});
|
|
5103
3699
|
cli.command("android run", "Build and run on Android device or emulator").option("-d, --device <id>", "Target device ID").option("-o, --output <dir>", "Android project directory", { default: "./android" }).example("craft android run").example("craft android run --device emulator-5554").action(async (options) => {
|
|
5104
|
-
const androidModule = await
|
|
3700
|
+
const androidModule = await import(androidModuleURL);
|
|
5105
3701
|
await androidModule.run({
|
|
5106
3702
|
device: options?.device,
|
|
5107
3703
|
output: options?.output || "./android"
|
|
@@ -5128,30 +3724,30 @@ cli.command("preview [path]", "Preview app in browser before building native").o
|
|
|
5128
3724
|
});
|
|
5129
3725
|
});
|
|
5130
3726
|
cli.command("publish", "Publish app to App Store or Play Store").option("--ios", "Publish to App Store (TestFlight)").option("--android", "Publish to Play Store").option("--api-key <path>", "App Store Connect API key path").option("--service-account <path>", "Google Play service account JSON").option("-o, --output <dir>", "Project directory", { default: "." }).example("craft publish --ios").example("craft publish --android").action(async (options) => {
|
|
5131
|
-
const { existsSync:
|
|
5132
|
-
const { join:
|
|
5133
|
-
const {
|
|
3727
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
3728
|
+
const { join: join2 } = await import("path");
|
|
3729
|
+
const { $ } = await Promise.resolve(globalThis.Bun);
|
|
5134
3730
|
if (options?.ios) {
|
|
5135
3731
|
console.log(`
|
|
5136
3732
|
\uD83D\uDCF1 Publishing to App Store (TestFlight)...
|
|
5137
3733
|
`);
|
|
5138
|
-
const iosDir =
|
|
5139
|
-
if (!
|
|
3734
|
+
const iosDir = join2(options?.output || ".", "ios");
|
|
3735
|
+
if (!existsSync5(iosDir)) {
|
|
5140
3736
|
console.error("\u274C No iOS project found. Run: craft ios init");
|
|
5141
3737
|
process6.exit(1);
|
|
5142
3738
|
}
|
|
5143
3739
|
console.log("1. Building archive...");
|
|
5144
3740
|
try {
|
|
5145
|
-
await
|
|
3741
|
+
await $`cd ${iosDir} && xcodebuild -scheme App -configuration Release -archivePath ./build/App.xcarchive archive`;
|
|
5146
3742
|
console.log("\u2705 Archive created");
|
|
5147
3743
|
console.log("2. Exporting IPA...");
|
|
5148
|
-
await
|
|
3744
|
+
await $`cd ${iosDir} && xcodebuild -exportArchive -archivePath ./build/App.xcarchive -exportPath ./build -exportOptionsPlist ExportOptions.plist`;
|
|
5149
3745
|
console.log("\u2705 IPA exported");
|
|
5150
3746
|
console.log("3. Uploading to TestFlight...");
|
|
5151
3747
|
if (options?.apiKey) {
|
|
5152
|
-
await
|
|
3748
|
+
await $`xcrun altool --upload-app -f ${iosDir}/build/*.ipa --apiKey ${options.apiKey} --type ios`;
|
|
5153
3749
|
} else {
|
|
5154
|
-
await
|
|
3750
|
+
await $`xcrun altool --upload-app -f ${iosDir}/build/*.ipa --type ios`;
|
|
5155
3751
|
}
|
|
5156
3752
|
console.log("\u2705 Uploaded to TestFlight!");
|
|
5157
3753
|
} catch (error) {
|
|
@@ -5165,16 +3761,16 @@ cli.command("publish", "Publish app to App Store or Play Store").option("--ios",
|
|
|
5165
3761
|
console.log(`
|
|
5166
3762
|
\uD83E\uDD16 Publishing to Play Store...
|
|
5167
3763
|
`);
|
|
5168
|
-
const androidDir =
|
|
5169
|
-
if (!
|
|
3764
|
+
const androidDir = join2(options?.output || ".", "android");
|
|
3765
|
+
if (!existsSync5(androidDir)) {
|
|
5170
3766
|
console.error("\u274C No Android project found. Run: craft android init");
|
|
5171
3767
|
process6.exit(1);
|
|
5172
3768
|
}
|
|
5173
3769
|
console.log("1. Building release AAB...");
|
|
5174
3770
|
try {
|
|
5175
|
-
await
|
|
3771
|
+
await $`cd ${androidDir} && ./gradlew bundleRelease`;
|
|
5176
3772
|
console.log("\u2705 AAB created");
|
|
5177
|
-
const aabPath =
|
|
3773
|
+
const aabPath = join2(androidDir, "app/build/outputs/bundle/release/app-release.aab");
|
|
5178
3774
|
if (options?.serviceAccount) {
|
|
5179
3775
|
console.log("2. Uploading to Play Store...");
|
|
5180
3776
|
console.log("\u26A0\uFE0F Automatic Play Store upload requires fastlane or Google Play API integration.");
|
|
@@ -5213,31 +3809,31 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
|
|
|
5213
3809
|
`);
|
|
5214
3810
|
const bundleId = options?.bundleId || `com.example.${name.toLowerCase().replace(/[^a-z0-9]/g, "")}`;
|
|
5215
3811
|
const appNameSlug = name.toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
5216
|
-
const { mkdirSync:
|
|
5217
|
-
const { join:
|
|
3812
|
+
const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync3, existsSync: existsSync5, readdirSync: readdirSync2, readFileSync: readFileSync3, cpSync: cpSync2 } = await import("fs");
|
|
3813
|
+
const { join: join2, dirname } = await import("path");
|
|
5218
3814
|
const replaceVars = (content) => {
|
|
5219
3815
|
return content.replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{APP_NAME_SLUG\}\}/g, appNameSlug).replace(/\{\{BUNDLE_ID\}\}/g, bundleId).replace(/\{\{AUTHOR\}\}/g, "Developer");
|
|
5220
3816
|
};
|
|
5221
3817
|
const copyTemplate = async (templateName, destDir) => {
|
|
5222
|
-
const templateDir =
|
|
5223
|
-
if (!
|
|
3818
|
+
const templateDir = join2(import.meta.dir, "templates/projects", templateName);
|
|
3819
|
+
if (!existsSync5(templateDir)) {
|
|
5224
3820
|
console.log(` \u26A0\uFE0F Template '${templateName}' not found, using blank template`);
|
|
5225
3821
|
return false;
|
|
5226
3822
|
}
|
|
5227
3823
|
const copyRecursive = (src, dest) => {
|
|
5228
|
-
if (!
|
|
5229
|
-
|
|
3824
|
+
if (!existsSync5(dest)) {
|
|
3825
|
+
mkdirSync2(dest, { recursive: true });
|
|
5230
3826
|
}
|
|
5231
|
-
const entries =
|
|
3827
|
+
const entries = readdirSync2(src, { withFileTypes: true });
|
|
5232
3828
|
for (const entry of entries) {
|
|
5233
|
-
const srcPath =
|
|
5234
|
-
const destPath =
|
|
3829
|
+
const srcPath = join2(src, entry.name);
|
|
3830
|
+
const destPath = join2(dest, entry.name);
|
|
5235
3831
|
if (entry.isDirectory()) {
|
|
5236
3832
|
copyRecursive(srcPath, destPath);
|
|
5237
3833
|
} else {
|
|
5238
|
-
const content =
|
|
3834
|
+
const content = readFileSync3(srcPath, "utf-8");
|
|
5239
3835
|
const processedContent = replaceVars(content);
|
|
5240
|
-
|
|
3836
|
+
writeFileSync3(destPath, processedContent);
|
|
5241
3837
|
}
|
|
5242
3838
|
}
|
|
5243
3839
|
};
|
|
@@ -5246,13 +3842,13 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
|
|
|
5246
3842
|
};
|
|
5247
3843
|
if (["blank", "tabs", "drawer", "dashboard"].includes(template)) {
|
|
5248
3844
|
console.log(`\uD83D\uDCC1 Creating ${template} project from template...`);
|
|
5249
|
-
if (!
|
|
5250
|
-
|
|
3845
|
+
if (!existsSync5(name)) {
|
|
3846
|
+
mkdirSync2(name, { recursive: true });
|
|
5251
3847
|
}
|
|
5252
3848
|
const copied = await copyTemplate(template, name);
|
|
5253
3849
|
if (!copied) {
|
|
5254
|
-
|
|
5255
|
-
|
|
3850
|
+
mkdirSync2(join2(name, "src"), { recursive: true });
|
|
3851
|
+
writeFileSync3(join2(name, "index.html"), replaceVars(`<!DOCTYPE html>
|
|
5256
3852
|
<html lang="en">
|
|
5257
3853
|
<head>
|
|
5258
3854
|
<meta charset="UTF-8">
|
|
@@ -5266,7 +3862,7 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
|
|
|
5266
3862
|
</div>
|
|
5267
3863
|
</body>
|
|
5268
3864
|
</html>`));
|
|
5269
|
-
|
|
3865
|
+
writeFileSync3(join2(name, "package.json"), replaceVars(JSON.stringify({
|
|
5270
3866
|
name: "{{APP_NAME_SLUG}}",
|
|
5271
3867
|
version: "1.0.0",
|
|
5272
3868
|
private: true,
|
|
@@ -5280,13 +3876,13 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
|
|
|
5280
3876
|
}
|
|
5281
3877
|
}, null, 2)));
|
|
5282
3878
|
}
|
|
5283
|
-
pinCraftNativeDependency(
|
|
3879
|
+
pinCraftNativeDependency(join2(name, "package.json"), version);
|
|
5284
3880
|
console.log(`\u2705 ${template} project created`);
|
|
5285
3881
|
}
|
|
5286
3882
|
if (template === "desktop" || template === "all") {
|
|
5287
3883
|
console.log("\uD83D\uDCC1 Creating desktop project structure...");
|
|
5288
|
-
if (!
|
|
5289
|
-
|
|
3884
|
+
if (!existsSync5(name)) {
|
|
3885
|
+
mkdirSync2(name, { recursive: true });
|
|
5290
3886
|
}
|
|
5291
3887
|
const configContent = `import type { CraftConfig } from 'craft-native'
|
|
5292
3888
|
|
|
@@ -5300,7 +3896,7 @@ export default {
|
|
|
5300
3896
|
},
|
|
5301
3897
|
} satisfies CraftConfig
|
|
5302
3898
|
`;
|
|
5303
|
-
|
|
3899
|
+
writeFileSync3(`${name}/craft.config.ts`, configContent);
|
|
5304
3900
|
const htmlContent = `<!DOCTYPE html>
|
|
5305
3901
|
<html lang="en">
|
|
5306
3902
|
<head>
|
|
@@ -5331,7 +3927,7 @@ export default {
|
|
|
5331
3927
|
</body>
|
|
5332
3928
|
</html>
|
|
5333
3929
|
`;
|
|
5334
|
-
|
|
3930
|
+
writeFileSync3(`${name}/index.html`, htmlContent);
|
|
5335
3931
|
const packageJson = {
|
|
5336
3932
|
name: appNameSlug,
|
|
5337
3933
|
version: "0.1.0",
|
|
@@ -5347,13 +3943,13 @@ export default {
|
|
|
5347
3943
|
"craft-native": `^${version}`
|
|
5348
3944
|
}
|
|
5349
3945
|
};
|
|
5350
|
-
|
|
3946
|
+
writeFileSync3(`${name}/package.json`, JSON.stringify(packageJson, null, 2));
|
|
5351
3947
|
pinCraftNativeDependency(`${name}/package.json`, version);
|
|
5352
3948
|
console.log("\u2705 Desktop project created");
|
|
5353
3949
|
}
|
|
5354
3950
|
if (template === "ios" || template === "all") {
|
|
5355
3951
|
console.log("\uD83D\uDCF1 Creating iOS project...");
|
|
5356
|
-
const iosModule = await
|
|
3952
|
+
const iosModule = await import(iosModuleURL);
|
|
5357
3953
|
await iosModule.init({
|
|
5358
3954
|
name,
|
|
5359
3955
|
bundleId: options?.bundleId,
|
|
@@ -5363,7 +3959,7 @@ export default {
|
|
|
5363
3959
|
}
|
|
5364
3960
|
if (template === "android" || template === "all") {
|
|
5365
3961
|
console.log("\uD83E\uDD16 Creating Android project...");
|
|
5366
|
-
const androidModule = await
|
|
3962
|
+
const androidModule = await import(androidModuleURL);
|
|
5367
3963
|
await androidModule.init({
|
|
5368
3964
|
name,
|
|
5369
3965
|
packageName: options?.bundleId,
|
|
@@ -5390,4 +3986,7 @@ export default {
|
|
|
5390
3986
|
});
|
|
5391
3987
|
cli.version(version);
|
|
5392
3988
|
cli.help();
|
|
3989
|
+
if ((process6.argv[2] === "ios" || process6.argv[2] === "android") && ["init", "build", "open", "run"].includes(process6.argv[3])) {
|
|
3990
|
+
process6.argv.splice(2, 2, `${process6.argv[2]} ${process6.argv[3]}`);
|
|
3991
|
+
}
|
|
5393
3992
|
cli.parse();
|