craft-native 0.0.369 → 0.0.374
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/templates/CraftNativeCapabilities.kt.template +50 -28
- package/dist/android/templates/CraftNativeSounds.kt.template +44 -19
- package/dist/api/mobile.d.ts +3 -1
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +6 -2
- package/dist/ios/src/index.js +338 -179
- package/dist/ios/src/runtime-profile.d.ts +13 -0
- package/dist/ios/templates/CraftApp.swift +279 -3
- package/dist/ios/templates/CraftBluetooth.swift +12 -0
- package/dist/ios/templates/CraftHealth.swift +124 -8
- package/dist/ios/templates/CraftHeartRate.swift +13 -0
- package/dist/ios/templates/CraftHybrid.swift +52 -11
- package/dist/ios/templates/CraftLiveActivities.swift +2 -0
- package/dist/ios/templates/CraftMusic.swift +23 -0
- package/dist/ios/templates/CraftNativeActions.swift +54 -8
- package/dist/ios/templates/CraftNativeFiles.swift +7 -0
- package/dist/ios/templates/CraftNativeScreen.swift +155 -0
- package/dist/ios/templates/CraftNativeServices.swift +19 -13
- package/dist/ios/templates/CraftVoiceRecording.swift +13 -1
- package/dist/mobile.js +6 -0
- package/package.json +1 -1
package/dist/ios/src/index.js
CHANGED
|
@@ -1,8 +1,114 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../ios/src/index.ts
|
|
3
3
|
import { gzipSync } from "zlib";
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
|
|
5
|
+
// ../ios/src/runtime-profile.ts
|
|
6
|
+
import { createHash } from "crypto";
|
|
7
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
8
|
+
import { tmpdir } from "os";
|
|
9
|
+
import { join, resolve } from "path";
|
|
10
|
+
var iosPermissionNames = ["speech", "microphone", "camera", "photos", "location", "contacts", "calendar", "bluetooth", "motion", "biometric", "music", "health", "nfc"];
|
|
11
|
+
function iosPermissions(config) {
|
|
12
|
+
const enabled = {
|
|
13
|
+
speech: config.enableSpeechRecognition,
|
|
14
|
+
microphone: config.enableSpeechRecognition || config.enableAudioRecording || config.enableVideoRecording,
|
|
15
|
+
camera: config.enableCamera || config.enableVideoRecording || config.enableQRScanner || config.enableAR,
|
|
16
|
+
photos: config.enableCamera || config.enableVideoRecording,
|
|
17
|
+
location: config.enableGeolocation || config.enableBackgroundLocation,
|
|
18
|
+
contacts: config.enableContacts,
|
|
19
|
+
calendar: config.enableCalendar,
|
|
20
|
+
bluetooth: config.enableBluetooth || config.enableHeartRate,
|
|
21
|
+
motion: config.enableMotionSensors,
|
|
22
|
+
biometric: config.enableBiometric,
|
|
23
|
+
music: config.enableAppleMusic,
|
|
24
|
+
health: config.enableHealthKit,
|
|
25
|
+
nfc: config.enableNFC
|
|
26
|
+
};
|
|
27
|
+
return iosPermissionNames.filter((name) => enabled[name] === true);
|
|
28
|
+
}
|
|
29
|
+
function iosPermissionMask(config) {
|
|
30
|
+
return iosPermissions(config).reduce((mask, name) => mask | 1 << iosPermissionNames.indexOf(name), 0);
|
|
31
|
+
}
|
|
32
|
+
function renderPermissionSource(source, config) {
|
|
33
|
+
const enabled = new Set(iosPermissions(config));
|
|
34
|
+
return source.replace(/#if CRAFT_(?:PERMISSION_([A-Z]+)|APPLE_(MUSIC))\b/g, (_match, permission, music) => {
|
|
35
|
+
const name = music ? "music" : permission.toLowerCase();
|
|
36
|
+
if (!iosPermissionNames.includes(name))
|
|
37
|
+
throw new Error(`Unknown Swift permission gate: ${name}`);
|
|
38
|
+
return `#if ${enabled.has(name) ? "true" : "false"}`;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
var archives = ["libcraft-ios.a", "libcraft-ios-simulator-arm64.a", "libcraft-ios-simulator-x64.a"];
|
|
42
|
+
var digest = (path) => createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
43
|
+
function verifyRuntimeProfile(directory, config) {
|
|
44
|
+
if (!existsSync(join(directory, "ios-permissions.json")))
|
|
45
|
+
throw new Error("Unprofiled Zig runtime: use runtimeSource to build archives for this app");
|
|
46
|
+
const profile = JSON.parse(readFileSync(join(directory, "ios-permissions.json"), "utf8"));
|
|
47
|
+
if (profile.version !== 1 || profile.mask !== iosPermissionMask(config))
|
|
48
|
+
throw new Error("Zig runtime permission profile does not match craft.config.json; rebuild it for this app");
|
|
49
|
+
if (!existsSync(join(directory, archives[0])) || !archives.slice(1).some((name) => existsSync(join(directory, name))))
|
|
50
|
+
throw new Error("Zig runtime must include device and simulator archives");
|
|
51
|
+
for (const name of archives) {
|
|
52
|
+
if (!existsSync(join(directory, name)) && !profile.sha256?.[name])
|
|
53
|
+
continue;
|
|
54
|
+
if (!existsSync(join(directory, name)) || profile.sha256?.[name] !== digest(join(directory, name)))
|
|
55
|
+
throw new Error(`Zig runtime archive identity mismatch: ${name}; rebuild it for this app`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function writeInstalledProfile(directory, config, source) {
|
|
59
|
+
const paths = ["device/libcraft-ios.a", "simulator/libcraft-ios.a"];
|
|
60
|
+
writeFileSync(join(directory, "profile.json"), JSON.stringify({
|
|
61
|
+
version: 1,
|
|
62
|
+
mask: iosPermissionMask(config),
|
|
63
|
+
source,
|
|
64
|
+
sha256: Object.fromEntries(paths.map((path) => [path, digest(join(directory, path))]))
|
|
65
|
+
}, null, 2));
|
|
66
|
+
}
|
|
67
|
+
function verifyInstalledProfile(directory, config) {
|
|
68
|
+
if (!existsSync(join(directory, "profile.json")))
|
|
69
|
+
throw new Error("Unprofiled Zig runtime: regenerate this project with runtimeSource before omitting permission descriptions");
|
|
70
|
+
const profile = JSON.parse(readFileSync(join(directory, "profile.json"), "utf8"));
|
|
71
|
+
if (profile.version !== 1 || profile.mask !== iosPermissionMask(config))
|
|
72
|
+
throw new Error("Installed Zig runtime permission profile is stale; rebuild it for craft.config.json");
|
|
73
|
+
for (const path of ["device/libcraft-ios.a", "simulator/libcraft-ios.a"]) {
|
|
74
|
+
if (!existsSync(join(directory, path)) || profile.sha256?.[path] !== digest(join(directory, path)))
|
|
75
|
+
throw new Error(`Installed Zig runtime identity mismatch: ${path}; rebuild it for this app`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function withAppRuntime(source, config, use) {
|
|
79
|
+
const root = resolve(source);
|
|
80
|
+
const manifest = readFileSync(join(root, "build.zig.zon"), "utf8");
|
|
81
|
+
const version = /\.minimum_zig_version\s*=\s*"([^"]+)"/.exec(manifest)?.[1];
|
|
82
|
+
if (!version)
|
|
83
|
+
throw new Error("Craft Zig source must declare its pinned minimum_zig_version");
|
|
84
|
+
const zig = process.env.CRAFT_ZIG || "zig";
|
|
85
|
+
const actual = Bun.spawnSync([zig, "version"], { stdout: "pipe", stderr: "pipe" });
|
|
86
|
+
if (actual.exitCode !== 0 || actual.stdout.toString().trim() !== version)
|
|
87
|
+
throw new Error(`Per-app iOS runtime requires Zig ${version}; set CRAFT_ZIG to that executable`);
|
|
88
|
+
const output = mkdtempSync(join(tmpdir(), "craft-ios-runtime-"));
|
|
89
|
+
try {
|
|
90
|
+
const names = iosPermissions(config);
|
|
91
|
+
const command = [zig, "build", "build-ios-all", `-Dios-permissions=${names.join(",") || "none"}`, "-Djs-runtime=false", "-Doptimize=ReleaseFast", "--prefix", output];
|
|
92
|
+
const child = Bun.spawn(command, { cwd: root, stdout: "inherit", stderr: "inherit" });
|
|
93
|
+
if (await child.exited !== 0)
|
|
94
|
+
throw new Error("Per-app iOS Zig runtime build failed; installed runtime was not replaced");
|
|
95
|
+
const directory = join(output, "lib");
|
|
96
|
+
const profile = JSON.parse(readFileSync(join(directory, "ios-permissions.json"), "utf8"));
|
|
97
|
+
if (profile.version !== 1 || profile.mask !== iosPermissionMask(config))
|
|
98
|
+
throw new Error("Craft Zig source did not build the requested iOS permission profile");
|
|
99
|
+
profile.sha256 = Object.fromEntries(archives.map((name) => [name, digest(join(directory, name))]));
|
|
100
|
+
writeFileSync(join(directory, "ios-permissions.json"), `${JSON.stringify(profile, null, 2)}
|
|
101
|
+
`);
|
|
102
|
+
verifyRuntimeProfile(directory, config);
|
|
103
|
+
return await use(directory);
|
|
104
|
+
} finally {
|
|
105
|
+
rmSync(output, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ../ios/src/index.ts
|
|
110
|
+
import { cpSync, existsSync as existsSync2, mkdirSync as mkdirSync2, mkdtempSync as mkdtempSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, renameSync, rmSync as rmSync3, statSync, writeFileSync as writeFileSync3 } from "fs";
|
|
111
|
+
import { dirname as dirname2, extname, join as join4, resolve as resolve2, sep as sep2 } from "path";
|
|
6
112
|
var {$ } = globalThis.Bun;
|
|
7
113
|
|
|
8
114
|
// ../ios/src/app-shortcuts.ts
|
|
@@ -61,7 +167,7 @@ ${entries}
|
|
|
61
167
|
|
|
62
168
|
// ../ios/src/source-audit.ts
|
|
63
169
|
import { readdirSync } from "fs";
|
|
64
|
-
import { join, relative, sep } from "path";
|
|
170
|
+
import { join as join2, relative, sep } from "path";
|
|
65
171
|
var defaultIgnoredPaths = [
|
|
66
172
|
".git",
|
|
67
173
|
"node_modules",
|
|
@@ -81,7 +187,7 @@ function isIgnored(path, ignoredPaths) {
|
|
|
81
187
|
}
|
|
82
188
|
function visit(root, current, ignoredPaths, found) {
|
|
83
189
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
84
|
-
const absolute =
|
|
190
|
+
const absolute = join2(current, entry.name);
|
|
85
191
|
const relativePath = relative(root, absolute);
|
|
86
192
|
if (isIgnored(relativePath, ignoredPaths))
|
|
87
193
|
continue;
|
|
@@ -108,11 +214,11 @@ ${sources.map((path) => `- ${path}`).join(`
|
|
|
108
214
|
}
|
|
109
215
|
}
|
|
110
216
|
// ../ios/src/testing.ts
|
|
111
|
-
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
112
|
-
import { tmpdir } from "os";
|
|
113
|
-
import { dirname, join as
|
|
217
|
+
import { mkdirSync, mkdtempSync as mkdtempSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
218
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
219
|
+
import { dirname, join as join3 } from "path";
|
|
114
220
|
import { spawnSync } from "child_process";
|
|
115
|
-
var templates =
|
|
221
|
+
var templates = join3(dirname(import.meta.dir), "templates/testing");
|
|
116
222
|
function bundleIdentifier(value) {
|
|
117
223
|
if (!/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(value))
|
|
118
224
|
throw new Error(`Invalid bundle identifier: ${value}`);
|
|
@@ -155,26 +261,26 @@ function initSimulatorDriver(options) {
|
|
|
155
261
|
["CraftTimedInput.m", "UITests/CraftTimedInput.m"],
|
|
156
262
|
["project.yml.template", "project.yml"]
|
|
157
263
|
]) {
|
|
158
|
-
const path =
|
|
264
|
+
const path = join3(output, destination);
|
|
159
265
|
mkdirSync(dirname(path), { recursive: true });
|
|
160
|
-
const content =
|
|
266
|
+
const content = readFileSync2(join3(templates, source), "utf8").replace(/\{\{([A-Z_]+)\}\}/g, (_, key) => replacements[key]);
|
|
161
267
|
let previous;
|
|
162
268
|
try {
|
|
163
|
-
previous =
|
|
269
|
+
previous = readFileSync2(path, "utf8");
|
|
164
270
|
} catch {}
|
|
165
271
|
if (previous !== content)
|
|
166
|
-
|
|
272
|
+
writeFileSync2(path, content);
|
|
167
273
|
}
|
|
168
|
-
const script =
|
|
274
|
+
const script = join3(output, "UITests/journey.txt");
|
|
169
275
|
if (options.script === undefined)
|
|
170
|
-
|
|
276
|
+
rmSync2(script, { force: true });
|
|
171
277
|
else {
|
|
172
278
|
let previous;
|
|
173
279
|
try {
|
|
174
|
-
previous =
|
|
280
|
+
previous = readFileSync2(script, "utf8");
|
|
175
281
|
} catch {}
|
|
176
282
|
if (previous !== options.script)
|
|
177
|
-
|
|
283
|
+
writeFileSync2(script, options.script);
|
|
178
284
|
}
|
|
179
285
|
}
|
|
180
286
|
function serializeSimulatorSteps(steps) {
|
|
@@ -267,16 +373,16 @@ function serializeSimulatorSteps(steps) {
|
|
|
267
373
|
`;
|
|
268
374
|
}
|
|
269
375
|
function createNativeMutationValidator() {
|
|
270
|
-
const output =
|
|
376
|
+
const output = mkdtempSync2(join3(tmpdir2(), `craft-native-mutation-p${process.pid}-`));
|
|
271
377
|
const removeOutput = () => {
|
|
272
378
|
process.off("exit", removeOutput);
|
|
273
|
-
|
|
379
|
+
rmSync2(output, { recursive: true, force: true });
|
|
274
380
|
};
|
|
275
381
|
process.on("exit", removeOutput);
|
|
276
|
-
const binary =
|
|
382
|
+
const binary = join3(output, "validate");
|
|
277
383
|
try {
|
|
278
|
-
|
|
279
|
-
const build = Bun.spawnSync(["swiftc",
|
|
384
|
+
writeFileSync2(join3(output, "main.swift"), readFileSync2(join3(templates, "MutationValidator.swift")));
|
|
385
|
+
const build = Bun.spawnSync(["swiftc", join3(templates, "../CraftNativeMutation.swift"), join3(output, "main.swift"), "-o", binary]);
|
|
280
386
|
if (build.exitCode !== 0)
|
|
281
387
|
throw new Error(build.stderr.toString());
|
|
282
388
|
} catch (error) {
|
|
@@ -288,14 +394,14 @@ function createNativeMutationValidator() {
|
|
|
288
394
|
validate(messages) {
|
|
289
395
|
if (disposed)
|
|
290
396
|
throw new Error("The native mutation validator has been disposed");
|
|
291
|
-
const input =
|
|
292
|
-
|
|
293
|
-
const report =
|
|
294
|
-
|
|
397
|
+
const input = join3(output, "messages.json");
|
|
398
|
+
writeFileSync2(input, JSON.stringify(messages));
|
|
399
|
+
const report = join3(output, "result.txt");
|
|
400
|
+
rmSync2(report, { force: true });
|
|
295
401
|
const result = spawnSync(binary, [input, report], { encoding: "utf8" });
|
|
296
402
|
let stdout = result.stdout ?? "";
|
|
297
403
|
try {
|
|
298
|
-
stdout =
|
|
404
|
+
stdout = readFileSync2(report, "utf8");
|
|
299
405
|
} catch {}
|
|
300
406
|
return { exitCode: result.status ?? 1, stdout, stderr: result.stderr || result.error?.message || (result.signal ? `Validator terminated by ${result.signal}` : "") };
|
|
301
407
|
},
|
|
@@ -310,20 +416,20 @@ function createNativeMutationValidator() {
|
|
|
310
416
|
function compressNativeSources(target) {
|
|
311
417
|
if (statSync(target).isDirectory()) {
|
|
312
418
|
for (const entry of readdirSync2(target))
|
|
313
|
-
compressNativeSources(
|
|
419
|
+
compressNativeSources(join4(target, entry));
|
|
314
420
|
return;
|
|
315
421
|
}
|
|
316
422
|
if (!/\.(?:js|json)$/.test(target))
|
|
317
423
|
return;
|
|
318
|
-
const bytes =
|
|
424
|
+
const bytes = readFileSync3(target);
|
|
319
425
|
if (bytes.length < 1024 || bytes[0] === 31 && bytes[1] === 139)
|
|
320
426
|
return;
|
|
321
427
|
const packed = gzipSync(bytes, { level: 9 });
|
|
322
428
|
if (packed.length < bytes.length)
|
|
323
|
-
|
|
429
|
+
writeFileSync3(target, packed);
|
|
324
430
|
}
|
|
325
431
|
function syncNativeFonts(fonts, output) {
|
|
326
|
-
const destination =
|
|
432
|
+
const destination = join4(output, "NativeFonts");
|
|
327
433
|
const names = new Set;
|
|
328
434
|
const files = Object.entries(fonts ?? {}).map(([family, entry]) => {
|
|
329
435
|
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(family))
|
|
@@ -331,32 +437,32 @@ function syncNativeFonts(fonts, output) {
|
|
|
331
437
|
if (names.has(family.toLowerCase()))
|
|
332
438
|
throw new Error(`Duplicate native font name: ${family}`);
|
|
333
439
|
names.add(family.toLowerCase());
|
|
334
|
-
if ([entry.src, entry.license].some((file) => file &&
|
|
440
|
+
if ([entry.src, entry.license].some((file) => file && resolve2(file).startsWith(resolve2(destination) + sep2)))
|
|
335
441
|
throw new Error("Native font sources must be outside the generated NativeFonts directory");
|
|
336
442
|
const extension = extname(entry.src).toLowerCase();
|
|
337
|
-
if (![".ttf", ".otf"].includes(extension) || !
|
|
443
|
+
if (![".ttf", ".otf"].includes(extension) || !existsSync2(entry.src) || !statSync(entry.src).isFile())
|
|
338
444
|
throw new Error(`Native font ${family} must reference an existing TTF or OTF file: ${entry.src}`);
|
|
339
|
-
if (entry.license && (!
|
|
445
|
+
if (entry.license && (!existsSync2(entry.license) || !statSync(entry.license).isFile()))
|
|
340
446
|
throw new Error(`Native font license not found: ${entry.license}`);
|
|
341
447
|
return { family, ...entry, filename: `${family}${extension}` };
|
|
342
448
|
});
|
|
343
|
-
|
|
449
|
+
rmSync3(destination, { recursive: true, force: true });
|
|
344
450
|
if (!files.length)
|
|
345
451
|
return [];
|
|
346
452
|
mkdirSync2(destination, { recursive: true });
|
|
347
453
|
for (const file of files) {
|
|
348
|
-
cpSync(file.src,
|
|
454
|
+
cpSync(file.src, join4(destination, file.filename));
|
|
349
455
|
if (file.license)
|
|
350
|
-
cpSync(file.license,
|
|
456
|
+
cpSync(file.license, join4(destination, `${file.family}.LICENSE.txt`));
|
|
351
457
|
}
|
|
352
458
|
return files.map((file) => `NativeFonts/${file.filename}`);
|
|
353
459
|
}
|
|
354
|
-
var TEMPLATES_DIR =
|
|
460
|
+
var TEMPLATES_DIR = join4(dirname2(import.meta.dir), "templates");
|
|
355
461
|
function resolveCraftVersion(startDir = import.meta.dir) {
|
|
356
|
-
let directory =
|
|
462
|
+
let directory = resolve2(startDir);
|
|
357
463
|
while (true) {
|
|
358
464
|
try {
|
|
359
|
-
const packageJSON = JSON.parse(
|
|
465
|
+
const packageJSON = JSON.parse(readFileSync3(join4(directory, "package.json"), "utf8"));
|
|
360
466
|
if ((packageJSON.name === "@craft-native/ios" || packageJSON.name === "craft-native") && typeof packageJSON.version === "string" && packageJSON.version.length > 0)
|
|
361
467
|
return packageJSON.version;
|
|
362
468
|
} catch {}
|
|
@@ -511,29 +617,29 @@ var nativeRouteBundles = true;
|
|
|
511
617
|
var nativeRouteRuntime = true;
|
|
512
618
|
var nativeMessages = true;
|
|
513
619
|
function installNativeBundle(source, output) {
|
|
514
|
-
if (!
|
|
620
|
+
if (!existsSync2(source))
|
|
515
621
|
throw new Error(`Native bundle not found: ${source}`);
|
|
516
|
-
const single =
|
|
517
|
-
const routes =
|
|
622
|
+
const single = join4(output, "dist", "native-screen.js");
|
|
623
|
+
const routes = join4(output, "dist", "native-screens");
|
|
518
624
|
mkdirSync2(dirname2(single), { recursive: true });
|
|
519
625
|
if (statSync(source).isDirectory()) {
|
|
520
626
|
if (!readdirSync2(source).some((name) => name.endsWith(".js")))
|
|
521
627
|
throw new Error(`Native route bundles not found in ${source}`);
|
|
522
|
-
if (
|
|
523
|
-
|
|
628
|
+
if (resolve2(source) !== resolve2(routes)) {
|
|
629
|
+
rmSync3(routes, { recursive: true, force: true });
|
|
524
630
|
cpSync(source, routes, { recursive: true });
|
|
525
631
|
}
|
|
526
632
|
compressNativeSources(routes);
|
|
527
|
-
|
|
633
|
+
rmSync3(single, { force: true });
|
|
528
634
|
return;
|
|
529
635
|
}
|
|
530
|
-
if (
|
|
636
|
+
if (resolve2(source) !== resolve2(single))
|
|
531
637
|
cpSync(source, single);
|
|
532
638
|
compressNativeSources(single);
|
|
533
|
-
|
|
639
|
+
rmSync3(routes, { recursive: true, force: true });
|
|
534
640
|
}
|
|
535
641
|
function hasInstalledNativeBundle(output) {
|
|
536
|
-
return
|
|
642
|
+
return existsSync2(join4(output, "dist", "native-screen.js")) || existsSync2(join4(output, "dist", "native-screens"));
|
|
537
643
|
}
|
|
538
644
|
function runPostGenerateHook(hook, output, phase = "build") {
|
|
539
645
|
if (!hook)
|
|
@@ -639,23 +745,10 @@ function renderAppearance(config) {
|
|
|
639
745
|
return { interfaceStyle: "Light", statusBarStyle: "UIStatusBarStyleDarkContent" };
|
|
640
746
|
return { interfaceStyle: "Dark", statusBarStyle: "UIStatusBarStyleLightContent" };
|
|
641
747
|
}
|
|
642
|
-
var LINKED_PURPOSES = {
|
|
643
|
-
NSSpeechRecognitionUsageDescription: "speech recognition",
|
|
644
|
-
NSMicrophoneUsageDescription: "the microphone",
|
|
645
|
-
NSCameraUsageDescription: "the camera",
|
|
646
|
-
NSPhotoLibraryUsageDescription: "your photo library",
|
|
647
|
-
NSLocationWhenInUseUsageDescription: "your location",
|
|
648
|
-
NSContactsUsageDescription: "your contacts",
|
|
649
|
-
NSCalendarsUsageDescription: "your calendar",
|
|
650
|
-
NSBluetoothAlwaysUsageDescription: "Bluetooth",
|
|
651
|
-
NSMotionUsageDescription: "motion data",
|
|
652
|
-
NSFaceIDUsageDescription: "Face ID",
|
|
653
|
-
NSAppleMusicUsageDescription: "your music"
|
|
654
|
-
};
|
|
655
748
|
function renderUsageDescriptions(config) {
|
|
656
749
|
const entries = [
|
|
657
750
|
[config.enableSpeechRecognition, "NSSpeechRecognitionUsageDescription", `${config.appName} uses speech recognition for voice input.`],
|
|
658
|
-
[config.enableSpeechRecognition || config.enableAudioRecording, "NSMicrophoneUsageDescription", `${config.appName} uses the microphone to record audio.`],
|
|
751
|
+
[config.enableSpeechRecognition || config.enableAudioRecording || config.enableVideoRecording, "NSMicrophoneUsageDescription", `${config.appName} uses the microphone to record audio.`],
|
|
659
752
|
[config.enableCamera || config.enableVideoRecording || config.enableQRScanner || config.enableAR, "NSCameraUsageDescription", `${config.appName} uses the camera for photos, video, scanning, and augmented reality.`],
|
|
660
753
|
[config.enableCamera || config.enableVideoRecording, "NSPhotoLibraryUsageDescription", `${config.appName} lets you choose photos and videos from your library.`],
|
|
661
754
|
[config.enableGeolocation || config.enableBackgroundLocation, "NSLocationWhenInUseUsageDescription", `${config.appName} uses your location while you record an activity.`],
|
|
@@ -678,8 +771,7 @@ function renderUsageDescriptions(config) {
|
|
|
678
771
|
return [key, custom];
|
|
679
772
|
if (enabled)
|
|
680
773
|
return [key, value];
|
|
681
|
-
|
|
682
|
-
return linked ? [key, `${config.appName} asks for ${linked} only in a feature that needs it.`] : null;
|
|
774
|
+
return null;
|
|
683
775
|
}).filter((entry) => entry !== null).map(([key, value]) => plistString(key, value)).join(`
|
|
684
776
|
`);
|
|
685
777
|
}
|
|
@@ -844,9 +936,11 @@ function renderRuntimeSettings() {
|
|
|
844
936
|
`);
|
|
845
937
|
}
|
|
846
938
|
function renderFeatureSettings(config) {
|
|
847
|
-
const conditions =
|
|
939
|
+
const conditions = iosPermissions(config).filter((name) => name !== "music").map((name) => `CRAFT_PERMISSION_${name.toUpperCase()}`);
|
|
848
940
|
if (config.enableScheduledWorkouts)
|
|
849
941
|
conditions.push("CRAFT_SCHEDULED_WORKOUTS");
|
|
942
|
+
if (config.enableAppleMusic)
|
|
943
|
+
conditions.push("CRAFT_APPLE_MUSIC");
|
|
850
944
|
if (!conditions.length)
|
|
851
945
|
return "";
|
|
852
946
|
return ` SWIFT_ACTIVE_COMPILATION_CONDITIONS: "$(inherited) ${conditions.join(" ")}"`;
|
|
@@ -921,7 +1015,7 @@ function pngHasAlpha(bytes) {
|
|
|
921
1015
|
return false;
|
|
922
1016
|
}
|
|
923
1017
|
function writeOpaqueIcon(source, target) {
|
|
924
|
-
const bytes = new Uint8Array(
|
|
1018
|
+
const bytes = new Uint8Array(readFileSync3(source));
|
|
925
1019
|
if (!pngHasAlpha(bytes) || process.platform !== "darwin") {
|
|
926
1020
|
cpSync(source, target);
|
|
927
1021
|
return;
|
|
@@ -930,27 +1024,27 @@ function writeOpaqueIcon(source, target) {
|
|
|
930
1024
|
const run = (args) => Bun.spawnSync(["sips", ...args], { stdout: "ignore", stderr: "pipe" });
|
|
931
1025
|
const toJpeg = run(["-s", "format", "jpeg", "-s", "formatOptions", "100", source, "--out", flattened]);
|
|
932
1026
|
const toPng = toJpeg.exitCode === 0 ? run(["-s", "format", "png", flattened, "--out", target]) : toJpeg;
|
|
933
|
-
|
|
1027
|
+
rmSync3(flattened, { force: true });
|
|
934
1028
|
if (toPng.exitCode !== 0) {
|
|
935
1029
|
cpSync(source, target);
|
|
936
1030
|
console.warn(`[craft] Could not remove the app icon's alpha channel; App Store Connect will refuse it: ${toPng.stderr.toString().trim()}`);
|
|
937
1031
|
}
|
|
938
1032
|
}
|
|
939
1033
|
function renderAssetCatalog(output, config) {
|
|
940
|
-
const catalog =
|
|
941
|
-
const appIcon =
|
|
942
|
-
const launchBackground =
|
|
1034
|
+
const catalog = join4(output, "Assets.xcassets");
|
|
1035
|
+
const appIcon = join4(catalog, "AppIcon.appiconset");
|
|
1036
|
+
const launchBackground = join4(catalog, "LaunchBackground.colorset");
|
|
943
1037
|
mkdirSync2(appIcon, { recursive: true });
|
|
944
1038
|
mkdirSync2(launchBackground, { recursive: true });
|
|
945
|
-
|
|
1039
|
+
writeFileSync3(join4(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
|
|
946
1040
|
`);
|
|
947
1041
|
const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
|
|
948
1042
|
if (config.appIconPath) {
|
|
949
|
-
if (!
|
|
1043
|
+
if (!existsSync2(config.appIconPath))
|
|
950
1044
|
throw new Error(`App icon not found: ${config.appIconPath}`);
|
|
951
|
-
writeOpaqueIcon(config.appIconPath,
|
|
1045
|
+
writeOpaqueIcon(config.appIconPath, join4(appIcon, iconFilename));
|
|
952
1046
|
}
|
|
953
|
-
|
|
1047
|
+
writeFileSync3(join4(appIcon, "Contents.json"), `${JSON.stringify({
|
|
954
1048
|
images: iconFilename ? [{
|
|
955
1049
|
filename: iconFilename,
|
|
956
1050
|
idiom: "universal",
|
|
@@ -977,7 +1071,7 @@ function renderAssetCatalog(output, config) {
|
|
|
977
1071
|
idiom: "universal"
|
|
978
1072
|
});
|
|
979
1073
|
}
|
|
980
|
-
|
|
1074
|
+
writeFileSync3(join4(launchBackground, "Contents.json"), `${JSON.stringify({
|
|
981
1075
|
colors,
|
|
982
1076
|
info: { author: "xcode", version: 1 }
|
|
983
1077
|
}, null, 2)}
|
|
@@ -986,8 +1080,8 @@ function renderAssetCatalog(output, config) {
|
|
|
986
1080
|
}
|
|
987
1081
|
var LAUNCH_LOGO = "LaunchLogo";
|
|
988
1082
|
function renderLaunchLogo(catalog, config) {
|
|
989
|
-
const imageset =
|
|
990
|
-
|
|
1083
|
+
const imageset = join4(catalog, `${LAUNCH_LOGO}.imageset`);
|
|
1084
|
+
rmSync3(imageset, { recursive: true, force: true });
|
|
991
1085
|
if (!config.splashImagePath)
|
|
992
1086
|
return;
|
|
993
1087
|
mkdirSync2(imageset, { recursive: true });
|
|
@@ -996,20 +1090,20 @@ function renderLaunchLogo(catalog, config) {
|
|
|
996
1090
|
for (const [source, dark] of [[config.splashImagePath, false], [config.splashImagePathDark, true]]) {
|
|
997
1091
|
if (!source)
|
|
998
1092
|
continue;
|
|
999
|
-
if (!
|
|
1093
|
+
if (!existsSync2(source))
|
|
1000
1094
|
throw new Error(`Splash image not found: ${source}`);
|
|
1001
1095
|
const extension = source.split(".").pop().toLowerCase();
|
|
1002
1096
|
if (extension === "svg" || extension === "pdf")
|
|
1003
1097
|
vector = true;
|
|
1004
1098
|
const filename = `${LAUNCH_LOGO}${dark ? "-dark" : ""}.${extension}`;
|
|
1005
|
-
cpSync(source,
|
|
1099
|
+
cpSync(source, join4(imageset, filename));
|
|
1006
1100
|
images.push({
|
|
1007
1101
|
...dark ? { appearances: [{ appearance: "luminosity", value: "dark" }] } : {},
|
|
1008
1102
|
filename,
|
|
1009
1103
|
idiom: "universal"
|
|
1010
1104
|
});
|
|
1011
1105
|
}
|
|
1012
|
-
|
|
1106
|
+
writeFileSync3(join4(imageset, "Contents.json"), `${JSON.stringify({
|
|
1013
1107
|
images,
|
|
1014
1108
|
info: { author: "xcode", version: 1 },
|
|
1015
1109
|
...vector ? { properties: { "preserves-vector-representation": true } } : {}
|
|
@@ -1120,18 +1214,18 @@ function renderOfflinePage(config) {
|
|
|
1120
1214
|
`;
|
|
1121
1215
|
}
|
|
1122
1216
|
function syncWebAssets(source, output) {
|
|
1123
|
-
const sourcePath =
|
|
1124
|
-
if (!
|
|
1217
|
+
const sourcePath = resolve2(source);
|
|
1218
|
+
if (!existsSync2(sourcePath))
|
|
1125
1219
|
throw new Error(`Web asset path not found: ${source}`);
|
|
1126
|
-
const distDir =
|
|
1127
|
-
|
|
1220
|
+
const distDir = join4(output, "dist");
|
|
1221
|
+
rmSync3(distDir, { recursive: true, force: true });
|
|
1128
1222
|
mkdirSync2(distDir, { recursive: true });
|
|
1129
1223
|
if (statSync(sourcePath).isDirectory()) {
|
|
1130
1224
|
cpSync(sourcePath, distDir, { recursive: true });
|
|
1131
1225
|
} else {
|
|
1132
|
-
cpSync(sourcePath,
|
|
1226
|
+
cpSync(sourcePath, join4(distDir, "index.html"));
|
|
1133
1227
|
}
|
|
1134
|
-
if (!
|
|
1228
|
+
if (!existsSync2(join4(distDir, "index.html"))) {
|
|
1135
1229
|
throw new Error(`Web asset directory must contain index.html: ${source}`);
|
|
1136
1230
|
}
|
|
1137
1231
|
}
|
|
@@ -1141,7 +1235,7 @@ function resolveRuntimeDir(override) {
|
|
|
1141
1235
|
const dir = override ?? process.env.CRAFT_IOS_RUNTIME;
|
|
1142
1236
|
if (!dir)
|
|
1143
1237
|
return null;
|
|
1144
|
-
if (!
|
|
1238
|
+
if (!existsSync2(dir)) {
|
|
1145
1239
|
const source = override === undefined ? "CRAFT_IOS_RUNTIME points at" : "runtimeDir is";
|
|
1146
1240
|
throw new Error(`${source} ${dir}, which does not exist.`);
|
|
1147
1241
|
}
|
|
@@ -1151,43 +1245,71 @@ var RUNTIME_ARCHIVES = {
|
|
|
1151
1245
|
device: ["libcraft-ios.a"],
|
|
1152
1246
|
simulator: ["libcraft-ios-simulator-arm64.a", "libcraft-ios-simulator-x64.a"]
|
|
1153
1247
|
};
|
|
1154
|
-
async function installRuntime(output, runtimeDir) {
|
|
1248
|
+
async function installRuntime(output, runtimeDir, config) {
|
|
1249
|
+
if (config)
|
|
1250
|
+
verifyRuntimeProfile(runtimeDir, config);
|
|
1155
1251
|
const resolved = Object.entries(RUNTIME_ARCHIVES).map(([sdk, archives]) => {
|
|
1156
|
-
const present = archives.filter((a) =>
|
|
1252
|
+
const present = archives.filter((a) => existsSync2(join4(runtimeDir, a)));
|
|
1157
1253
|
if (present.length === 0) {
|
|
1158
1254
|
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.`);
|
|
1159
1255
|
}
|
|
1160
1256
|
return { sdk, archives, present };
|
|
1161
1257
|
});
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
const
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1258
|
+
const installed = join4(output, "Runtime");
|
|
1259
|
+
const staging = mkdtempSync3(join4(output, ".craft-runtime-"));
|
|
1260
|
+
const dest = join4(staging, "next");
|
|
1261
|
+
const previous = join4(staging, "previous");
|
|
1262
|
+
try {
|
|
1263
|
+
for (const { sdk, archives, present } of resolved) {
|
|
1264
|
+
const sdkDir = join4(dest, sdk);
|
|
1265
|
+
mkdirSync2(sdkDir, { recursive: true });
|
|
1266
|
+
const target = join4(sdkDir, "libcraft-ios.a");
|
|
1267
|
+
if (present.length === 1) {
|
|
1268
|
+
const missing = archives.filter((a) => !present.includes(a));
|
|
1269
|
+
if (missing.length > 0) {
|
|
1270
|
+
console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
|
|
1271
|
+
}
|
|
1272
|
+
cpSync(join4(runtimeDir, present[0]), target);
|
|
1273
|
+
} else {
|
|
1274
|
+
await $`lipo -create ${present.map((a) => join4(runtimeDir, a))} -output ${target}`.quiet();
|
|
1172
1275
|
}
|
|
1173
|
-
cpSync(join3(runtimeDir, present[0]), target);
|
|
1174
|
-
} else {
|
|
1175
|
-
await $`lipo -create ${present.map((a) => join3(runtimeDir, a))} -output ${target}`.quiet();
|
|
1176
1276
|
}
|
|
1277
|
+
if (config)
|
|
1278
|
+
writeInstalledProfile(dest, config);
|
|
1279
|
+
if (existsSync2(installed))
|
|
1280
|
+
renameSync(installed, previous);
|
|
1281
|
+
try {
|
|
1282
|
+
renameSync(dest, installed);
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
if (existsSync2(previous))
|
|
1285
|
+
renameSync(previous, installed);
|
|
1286
|
+
throw error;
|
|
1287
|
+
}
|
|
1288
|
+
return true;
|
|
1289
|
+
} finally {
|
|
1290
|
+
rmSync3(staging, { recursive: true, force: true });
|
|
1177
1291
|
}
|
|
1178
|
-
return true;
|
|
1179
1292
|
}
|
|
1180
|
-
async function refreshRuntime(output, override) {
|
|
1181
|
-
if (!
|
|
1293
|
+
async function refreshRuntime(output, config, override) {
|
|
1294
|
+
if (!existsSync2(join4(output, "Runtime")))
|
|
1182
1295
|
return;
|
|
1183
1296
|
const dir = resolveRuntimeDir(override);
|
|
1184
1297
|
if (!dir) {
|
|
1298
|
+
verifyInstalledProfile(join4(output, "Runtime"), config);
|
|
1185
1299
|
console.log(" Keeping the Zig runtime installed at init (no runtime directory configured)");
|
|
1186
1300
|
return;
|
|
1187
1301
|
}
|
|
1188
|
-
await installRuntime(output, dir);
|
|
1302
|
+
await installRuntime(output, dir, config);
|
|
1189
1303
|
console.log(" Refreshed the Zig runtime from", dir);
|
|
1190
1304
|
}
|
|
1305
|
+
async function installAppRuntime(output, source, config) {
|
|
1306
|
+
return withAppRuntime(source, config, async (directory) => {
|
|
1307
|
+
verifyRuntimeProfile(directory, config);
|
|
1308
|
+
await installRuntime(output, directory, config);
|
|
1309
|
+
writeInstalledProfile(join4(output, "Runtime"), config, resolve2(source));
|
|
1310
|
+
return true;
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1191
1313
|
function productName(name) {
|
|
1192
1314
|
const words = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
1193
1315
|
const joined = words.map((word) => word[0].toUpperCase() + word.slice(1)).join("");
|
|
@@ -1196,8 +1318,10 @@ function productName(name) {
|
|
|
1196
1318
|
return /^\d/.test(joined) ? `App${joined}` : joined;
|
|
1197
1319
|
}
|
|
1198
1320
|
function restoreShell(output) {
|
|
1199
|
-
const file =
|
|
1200
|
-
const config = JSON.parse(
|
|
1321
|
+
const file = join4(output, "craft.config.json");
|
|
1322
|
+
const config = JSON.parse(readFileSync3(file, "utf8"));
|
|
1323
|
+
if (existsSync2(join4(output, "Runtime")))
|
|
1324
|
+
verifyInstalledProfile(join4(output, "Runtime"), config);
|
|
1201
1325
|
const cloudBuild = process.env.CI_BUILD_NUMBER;
|
|
1202
1326
|
if (cloudBuild !== undefined) {
|
|
1203
1327
|
if (!/^[1-9]\d*$/.test(cloudBuild) || cloudBuild.trim() !== cloudBuild)
|
|
@@ -1205,51 +1329,70 @@ function restoreShell(output) {
|
|
|
1205
1329
|
config.buildNumber = cloudBuild;
|
|
1206
1330
|
}
|
|
1207
1331
|
writeShell(output, config);
|
|
1332
|
+
refreshUsageDescriptions(output, config);
|
|
1208
1333
|
if (cloudBuild !== undefined)
|
|
1209
|
-
|
|
1334
|
+
writeFileSync3(file, JSON.stringify(config, null, 2));
|
|
1210
1335
|
}
|
|
1211
1336
|
function writeShell(output, config) {
|
|
1212
1337
|
if (!config.appName || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(config.bundleId))
|
|
1213
1338
|
throw new Error("The iOS shell needs an appName and a valid bundleId in craft.config.json.");
|
|
1339
|
+
const writeSource = (path, source) => {
|
|
1340
|
+
const rendered = renderPermissionSource(source, config);
|
|
1341
|
+
if (!existsSync2(path) || readFileSync3(path, "utf8") !== rendered)
|
|
1342
|
+
writeFileSync3(path, rendered);
|
|
1343
|
+
};
|
|
1214
1344
|
const name = productName(config.appName);
|
|
1215
1345
|
const shortcutSource = appShortcutsSwift(config.appShortcuts, config.urlSchemes);
|
|
1216
|
-
mkdirSync2(
|
|
1217
|
-
mkdirSync2(
|
|
1218
|
-
const swiftTemplate =
|
|
1346
|
+
mkdirSync2(join4(output, "Sources"), { recursive: true });
|
|
1347
|
+
mkdirSync2(join4(output, "Shared"), { recursive: true });
|
|
1348
|
+
const swiftTemplate = readFileSync3(join4(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
|
|
1219
1349
|
const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, config.bundleId).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION);
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1350
|
+
writeSource(join4(output, "Sources", `${name}App.swift`), swiftSource);
|
|
1351
|
+
writeSource(join4(output, "Sources", "CraftAppShortcuts.swift"), shortcutSource);
|
|
1352
|
+
writeSource(join4(output, "Sources", "CraftNativeScreen.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftNativeScreen.swift"), "utf8"));
|
|
1353
|
+
writeSource(join4(output, "Sources", "CraftNativeFlatList.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftNativeFlatList.swift"), "utf8"));
|
|
1354
|
+
writeSource(join4(output, "Sources", "CraftNativeActions.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftNativeActions.swift"), "utf8"));
|
|
1355
|
+
writeSource(join4(output, "Sources", "CraftHealth.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftHealth.swift"), "utf8"));
|
|
1356
|
+
writeSource(join4(output, "Sources", "CraftMusic.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftMusic.swift"), "utf8"));
|
|
1357
|
+
writeSource(join4(output, "Sources", "CraftBluetooth.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftBluetooth.swift"), "utf8"));
|
|
1358
|
+
writeSource(join4(output, "Sources", "CraftHeartRate.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftHeartRate.swift"), "utf8"));
|
|
1359
|
+
writeSource(join4(output, "Sources", "CraftRecordingArchive.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftRecordingArchive.swift"), "utf8"));
|
|
1360
|
+
writeSource(join4(output, "Sources", "CraftPersistentStorage.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftPersistentStorage.swift"), "utf8"));
|
|
1361
|
+
writeSource(join4(output, "Sources", "CraftScheduledWorkouts.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftScheduledWorkouts.swift"), "utf8"));
|
|
1362
|
+
writeSource(join4(output, "Sources", "CraftNativeMutation.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftNativeMutation.swift"), "utf8"));
|
|
1363
|
+
writeSource(join4(output, "Sources", "CraftNativeUI.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftNativeUI.swift"), "utf8"));
|
|
1364
|
+
writeSource(join4(output, "Sources", "CraftNativeServices.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftNativeServices.swift"), "utf8"));
|
|
1365
|
+
writeSource(join4(output, "Sources", "CraftNativeFiles.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftNativeFiles.swift"), "utf8"));
|
|
1366
|
+
writeSource(join4(output, "Sources", "CraftVoiceRecording.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftVoiceRecording.swift"), "utf8"));
|
|
1367
|
+
writeSource(join4(output, "Sources", "CraftWebAudio.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftWebAudio.swift"), "utf8"));
|
|
1368
|
+
writeSource(join4(output, "Sources", "CraftHybrid.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftHybrid.swift"), "utf8"));
|
|
1369
|
+
writeSource(join4(output, "Sources", "CraftCrashReporter.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftCrashReporter.swift"), "utf-8").replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION));
|
|
1370
|
+
writeSource(join4(output, "Sources", "CraftLiveActivities.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftLiveActivities.swift"), "utf8"));
|
|
1371
|
+
writeSource(join4(output, "Shared", "CraftActivityAttributes.swift"), readFileSync3(join4(TEMPLATES_DIR, "CraftActivityAttributes.swift"), "utf8"));
|
|
1242
1372
|
for (const [enabled, folder, template, filename] of [
|
|
1243
1373
|
[config.enableLiveActivities, "WidgetExtension", "CraftLiveActivityWidget.swift.template", `${name}LiveActivity.swift`],
|
|
1244
1374
|
[config.enableWatchApp, "WatchExtension", "CraftWatchApp.swift.template", `${name}WatchApp.swift`]
|
|
1245
1375
|
]) {
|
|
1246
1376
|
if (!enabled)
|
|
1247
1377
|
continue;
|
|
1248
|
-
mkdirSync2(
|
|
1249
|
-
const source =
|
|
1250
|
-
|
|
1378
|
+
mkdirSync2(join4(output, folder), { recursive: true });
|
|
1379
|
+
const source = readFileSync3(join4(TEMPLATES_DIR, template), "utf8").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{HEALTH_KIT_ENABLED\}\}/g, config.enableHealthKit ? "true" : "false");
|
|
1380
|
+
writeSource(join4(output, folder, filename), source);
|
|
1251
1381
|
}
|
|
1252
1382
|
}
|
|
1383
|
+
function refreshUsageDescriptions(output, config) {
|
|
1384
|
+
const path = join4(output, "Info.plist");
|
|
1385
|
+
if (!existsSync2(path))
|
|
1386
|
+
return;
|
|
1387
|
+
const managed = /\s*<key>(?:NS(?:SpeechRecognition|Microphone|Camera|PhotoLibrary|LocationWhenInUse|LocationAlwaysAndWhenInUse|Contacts|Calendars|BluetoothAlways|Motion|LocalNetwork|HealthShare|HealthUpdate|FaceID|AppleMusic)|NFCReader)UsageDescription<\/key>\s*<string>[\s\S]*?<\/string>/g;
|
|
1388
|
+
const plist = readFileSync3(path, "utf8").replace(managed, "");
|
|
1389
|
+
const end = plist.lastIndexOf("</dict>");
|
|
1390
|
+
if (end < 0)
|
|
1391
|
+
throw new Error("Info.plist must contain a root dictionary");
|
|
1392
|
+
writeFileSync3(path, `${plist.slice(0, end).trimEnd()}
|
|
1393
|
+
${renderUsageDescriptions(config)}
|
|
1394
|
+
${plist.slice(end)}`);
|
|
1395
|
+
}
|
|
1253
1396
|
async function init(options) {
|
|
1254
1397
|
const { name: displayName, bundleId, teamId, output } = options;
|
|
1255
1398
|
const name = productName(displayName);
|
|
@@ -1257,11 +1400,11 @@ async function init(options) {
|
|
|
1257
1400
|
\u26A1 Initializing Craft iOS project: ${displayName}`);
|
|
1258
1401
|
console.log(` Output: ${output}
|
|
1259
1402
|
`);
|
|
1260
|
-
const dirs = [output,
|
|
1403
|
+
const dirs = [output, join4(output, "Sources"), join4(output, "Shared"), join4(output, "dist")];
|
|
1261
1404
|
if (options.config?.enableWatchApp)
|
|
1262
|
-
dirs.push(
|
|
1405
|
+
dirs.push(join4(output, "WatchApp"), join4(output, "WatchExtension"));
|
|
1263
1406
|
for (const dir of dirs) {
|
|
1264
|
-
if (!
|
|
1407
|
+
if (!existsSync2(dir)) {
|
|
1265
1408
|
mkdirSync2(dir, { recursive: true });
|
|
1266
1409
|
}
|
|
1267
1410
|
}
|
|
@@ -1291,10 +1434,20 @@ async function init(options) {
|
|
|
1291
1434
|
`));
|
|
1292
1435
|
if (Object.keys(config.shareStorage ?? {}).length)
|
|
1293
1436
|
config.enableSecureStorage = true;
|
|
1294
|
-
|
|
1437
|
+
const runtimeDir = resolveRuntimeDir(options.runtimeDir);
|
|
1438
|
+
const runtimeSource = options.runtimeSource === null || options.runtimeDir === null ? undefined : options.runtimeSource ?? process.env.CRAFT_IOS_RUNTIME_SOURCE;
|
|
1439
|
+
if (runtimeSource && runtimeDir)
|
|
1440
|
+
throw new Error("Choose runtimeSource or runtimeDir, not both");
|
|
1441
|
+
const hasRuntime = runtimeSource ? await installAppRuntime(output, runtimeSource, config) : runtimeDir ? await installRuntime(output, runtimeDir, config) : false;
|
|
1442
|
+
if (hasRuntime) {
|
|
1443
|
+
console.log(" Linked the Zig runtime from", runtimeSource ?? runtimeDir);
|
|
1444
|
+
} else {
|
|
1445
|
+
rmSync3(join4(output, "Runtime"), { recursive: true, force: true });
|
|
1446
|
+
}
|
|
1447
|
+
writeFileSync3(join4(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
1295
1448
|
writeShell(output, config);
|
|
1296
1449
|
const nativeFontFiles = syncNativeFonts(config.nativeFonts, output);
|
|
1297
|
-
const infoPlistTemplate =
|
|
1450
|
+
const infoPlistTemplate = readFileSync3(join4(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
|
|
1298
1451
|
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(/\{\{APP_BOUND_DOMAINS\}\}/g, renderAppBoundDomains(config)).replace(/\{\{BACKGROUND_MODES\}\}/g, renderBackgroundModes(config)).replace(/\{\{BACKGROUND_TASK_IDENTIFIERS\}\}/g, renderBackgroundTaskIdentifiers(config)).replace(/\{\{LAUNCH_SCREEN\}\}/g, renderLaunchScreen(config)).replace(/\{\{NATIVE_FONTS\}\}/g, nativeFontFiles.length ? ` <key>UIAppFonts</key>
|
|
1299
1452
|
<array>
|
|
1300
1453
|
${plistArray(nativeFontFiles, 2)}
|
|
@@ -1302,8 +1455,8 @@ ${plistArray(nativeFontFiles, 2)}
|
|
|
1302
1455
|
<true/>
|
|
1303
1456
|
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
|
|
1304
1457
|
<true/>` : "");
|
|
1305
|
-
|
|
1306
|
-
const projectYmlTemplate =
|
|
1458
|
+
writeFileSync3(join4(output, "Info.plist"), infoPlist);
|
|
1459
|
+
const projectYmlTemplate = readFileSync3(join4(TEMPLATES_DIR, "project.yml.template"), "utf-8");
|
|
1307
1460
|
const nativeDependencies = [];
|
|
1308
1461
|
if (config.enableLiveActivities)
|
|
1309
1462
|
nativeDependencies.push(` - target: ${name}LiveActivity`);
|
|
@@ -1358,38 +1511,31 @@ ${plistArray(nativeFontFiles, 2)}
|
|
|
1358
1511
|
APPLICATION_EXTENSION_API_ONLY: YES
|
|
1359
1512
|
SKIP_INSTALL: YES`);
|
|
1360
1513
|
}
|
|
1361
|
-
const runtimeDir = resolveRuntimeDir(options.runtimeDir);
|
|
1362
|
-
const hasRuntime = runtimeDir ? await installRuntime(output, runtimeDir) : false;
|
|
1363
|
-
if (hasRuntime) {
|
|
1364
|
-
console.log(" Linked the Zig runtime from", runtimeDir);
|
|
1365
|
-
} else {
|
|
1366
|
-
rmSync2(join3(output, "Runtime"), { recursive: true, force: true });
|
|
1367
|
-
}
|
|
1368
1514
|
const projectYml = projectYmlTemplate.replace(/\{\{NATIVE_FONT_RESOURCES\}\}/g, nativeFontFiles.length ? ` - path: NativeFonts
|
|
1369
1515
|
type: folder
|
|
1370
1516
|
buildPhase: resources` : "").replace(/\{\{CRAFT_RUNTIME_SETTINGS\}\}/g, hasRuntime ? renderRuntimeSettings() : "").replace(/\{\{FEATURE_SETTINGS\}\}/g, renderFeatureSettings(config)).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:
|
|
1371
1517
|
${nativeDependencies.join(`
|
|
1372
1518
|
`)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
|
|
1373
1519
|
`));
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1520
|
+
writeFileSync3(join4(output, "project.yml"), projectYml);
|
|
1521
|
+
writeFileSync3(join4(output, "Craft.entitlements"), renderEntitlements(config));
|
|
1522
|
+
writeFileSync3(join4(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
|
|
1377
1523
|
renderAssetCatalog(output, config);
|
|
1378
1524
|
if (config.enableLiveActivities) {
|
|
1379
|
-
mkdirSync2(
|
|
1380
|
-
const widgetInfo =
|
|
1381
|
-
|
|
1525
|
+
mkdirSync2(join4(output, "WidgetExtension"), { recursive: true });
|
|
1526
|
+
const widgetInfo = readFileSync3(join4(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
|
|
1527
|
+
writeFileSync3(join4(output, "WidgetExtension", "Info.plist"), widgetInfo);
|
|
1382
1528
|
}
|
|
1383
1529
|
if (config.enableWatchApp) {
|
|
1384
|
-
const watchInfo =
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
const watchExtensionInfo =
|
|
1530
|
+
const watchInfo = readFileSync3(join4(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
1531
|
+
writeFileSync3(join4(output, "WatchApp", "Info.plist"), watchInfo);
|
|
1532
|
+
writeFileSync3(join4(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
1533
|
+
const watchExtensionInfo = readFileSync3(join4(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{HEALTH_USAGE_DESCRIPTIONS\}\}/g, config.enableHealthKit ? `${plistString("NSHealthShareUsageDescription", `${displayName} reads health data you choose to share.`)}
|
|
1388
1534
|
${plistString("NSHealthUpdateUsageDescription", `${displayName} writes health data only with your permission.`)}` : "");
|
|
1389
|
-
|
|
1390
|
-
|
|
1535
|
+
writeFileSync3(join4(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
|
|
1536
|
+
writeFileSync3(join4(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
1391
1537
|
}
|
|
1392
|
-
|
|
1538
|
+
writeFileSync3(join4(output, "dist", "index.html"), renderOfflinePage(config));
|
|
1393
1539
|
if (usesNativeBundle(config)) {
|
|
1394
1540
|
if (config.nativeBundle)
|
|
1395
1541
|
installNativeBundle(config.nativeBundle, output);
|
|
@@ -1416,11 +1562,11 @@ async function build(options) {
|
|
|
1416
1562
|
const { htmlPath, nativeBundlePath, devServer, output, generateProject = true } = options;
|
|
1417
1563
|
console.log(`
|
|
1418
1564
|
\uD83D\uDCE6 Building Craft iOS project...`);
|
|
1419
|
-
const configPath =
|
|
1420
|
-
if (!
|
|
1565
|
+
const configPath = join4(output, "craft.config.json");
|
|
1566
|
+
if (!existsSync2(configPath)) {
|
|
1421
1567
|
throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
|
|
1422
1568
|
}
|
|
1423
|
-
const config = JSON.parse(
|
|
1569
|
+
const config = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
1424
1570
|
if (config.renderer !== undefined && config.renderer !== "web" && config.renderer !== "native") {
|
|
1425
1571
|
throw new Error(`Unknown iOS renderer: ${config.renderer}. Expected web or native.`);
|
|
1426
1572
|
}
|
|
@@ -1445,7 +1591,7 @@ async function build(options) {
|
|
|
1445
1591
|
config.devServerURL = devServer;
|
|
1446
1592
|
const origin = new URL(devServer).origin;
|
|
1447
1593
|
config.trustedOrigins = [...new Set([...config.trustedOrigins ?? [], origin])];
|
|
1448
|
-
|
|
1594
|
+
writeFileSync3(configPath, JSON.stringify(config, null, 2));
|
|
1449
1595
|
console.log(` Dev server: ${devServer}`);
|
|
1450
1596
|
}
|
|
1451
1597
|
if (htmlPath) {
|
|
@@ -1461,7 +1607,20 @@ async function build(options) {
|
|
|
1461
1607
|
console.warn(" \u26A0 nativeScreens is set but there is no native bundle: every path opens in the web view.");
|
|
1462
1608
|
}
|
|
1463
1609
|
}
|
|
1464
|
-
|
|
1610
|
+
const profilePath = join4(output, "Runtime", "profile.json");
|
|
1611
|
+
const savedProfile = existsSync2(profilePath) ? JSON.parse(readFileSync3(profilePath, "utf8")) : undefined;
|
|
1612
|
+
const runtimeSource = options.runtimeSource === null || options.runtimeDir === null ? undefined : options.runtimeSource ?? process.env.CRAFT_IOS_RUNTIME_SOURCE ?? savedProfile?.source;
|
|
1613
|
+
if (runtimeSource && existsSync2(join4(output, "Runtime"))) {
|
|
1614
|
+
if (options.runtimeDir)
|
|
1615
|
+
throw new Error("Choose runtimeSource or runtimeDir, not both");
|
|
1616
|
+
await installAppRuntime(output, runtimeSource, config);
|
|
1617
|
+
} else {
|
|
1618
|
+
if (savedProfile && savedProfile.mask !== iosPermissionMask(config))
|
|
1619
|
+
throw new Error("Installed Zig runtime permission profile is stale; provide runtimeSource to rebuild it");
|
|
1620
|
+
await refreshRuntime(output, config, options.runtimeDir);
|
|
1621
|
+
}
|
|
1622
|
+
writeShell(output, config);
|
|
1623
|
+
refreshUsageDescriptions(output, config);
|
|
1465
1624
|
if (!generateProject)
|
|
1466
1625
|
return;
|
|
1467
1626
|
try {
|
|
@@ -1488,7 +1647,7 @@ async function open(options) {
|
|
|
1488
1647
|
if (!xcodeproj) {
|
|
1489
1648
|
throw new Error(`No Xcode project found in ${output}. Run 'craft ios build' first.`);
|
|
1490
1649
|
}
|
|
1491
|
-
const projectPath =
|
|
1650
|
+
const projectPath = join4(output, xcodeproj);
|
|
1492
1651
|
console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
|
|
1493
1652
|
await $`open ${projectPath}`;
|
|
1494
1653
|
}
|
|
@@ -1542,8 +1701,8 @@ async function showSimulator() {
|
|
|
1542
1701
|
const selected = await $`xcode-select -p`.quiet().nothrow();
|
|
1543
1702
|
const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
|
|
1544
1703
|
if (developer) {
|
|
1545
|
-
const app =
|
|
1546
|
-
if (
|
|
1704
|
+
const app = join4(developer, "Applications", "Simulator.app");
|
|
1705
|
+
if (existsSync2(app)) {
|
|
1547
1706
|
await $`open ${app}`.quiet().nothrow();
|
|
1548
1707
|
return;
|
|
1549
1708
|
}
|
|
@@ -1558,12 +1717,12 @@ async function run(options) {
|
|
|
1558
1717
|
if (!xcodeproj) {
|
|
1559
1718
|
throw new Error(`No Xcode project found in ${output}`);
|
|
1560
1719
|
}
|
|
1561
|
-
const projectPath =
|
|
1720
|
+
const projectPath = join4(output, xcodeproj);
|
|
1562
1721
|
const appName = xcodeproj.replace(".xcodeproj", "");
|
|
1563
|
-
const configPath =
|
|
1564
|
-
if (!
|
|
1722
|
+
const configPath = join4(output, "craft.config.json");
|
|
1723
|
+
if (!existsSync2(configPath))
|
|
1565
1724
|
throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
|
|
1566
|
-
const config = JSON.parse(
|
|
1725
|
+
const config = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
1567
1726
|
if (simulator) {
|
|
1568
1727
|
console.log("\uD83D\uDCF1 Building and running on simulator...");
|
|
1569
1728
|
const device = await pickSimulator();
|
|
@@ -1573,10 +1732,10 @@ async function run(options) {
|
|
|
1573
1732
|
console.log(` Device: ${device.name} (${device.runtime})`);
|
|
1574
1733
|
try {
|
|
1575
1734
|
await bootSimulator(device);
|
|
1576
|
-
const derivedData =
|
|
1577
|
-
const product =
|
|
1735
|
+
const derivedData = join4(output, "build");
|
|
1736
|
+
const product = join4(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
|
|
1578
1737
|
await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
|
|
1579
|
-
if (!
|
|
1738
|
+
if (!existsSync2(product)) {
|
|
1580
1739
|
throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
|
|
1581
1740
|
}
|
|
1582
1741
|
await $`xcrun simctl install ${device.udid} ${product}`;
|