@craft-native/ios 0.0.368 → 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/README.md +48 -3
- package/dist/cli.js +314 -158
- package/dist/index.d.ts +6 -2
- package/dist/index.js +338 -179
- package/dist/runtime-profile.d.ts +13 -0
- package/package.json +1 -1
- package/templates/CraftApp.swift +279 -3
- package/templates/CraftBluetooth.swift +12 -0
- package/templates/CraftHealth.swift +124 -8
- package/templates/CraftHeartRate.swift +13 -0
- package/templates/CraftHybrid.swift +52 -11
- package/templates/CraftLiveActivities.swift +2 -0
- package/templates/CraftMusic.swift +23 -0
- package/templates/CraftNativeActions.swift +54 -8
- package/templates/CraftNativeFiles.swift +7 -0
- package/templates/CraftNativeScreen.swift +155 -0
- package/templates/CraftNativeServices.swift +19 -13
- package/templates/CraftVoiceRecording.swift +13 -1
package/dist/cli.js
CHANGED
|
@@ -3,12 +3,118 @@
|
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
5
|
import { parseArgs } from "util";
|
|
6
|
-
import { resolve as
|
|
6
|
+
import { resolve as resolve3 } from "path";
|
|
7
7
|
|
|
8
8
|
// src/index.ts
|
|
9
9
|
import { gzipSync } from "zlib";
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
|
|
11
|
+
// src/runtime-profile.ts
|
|
12
|
+
import { createHash } from "crypto";
|
|
13
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
14
|
+
import { tmpdir } from "os";
|
|
15
|
+
import { join, resolve } from "path";
|
|
16
|
+
var iosPermissionNames = ["speech", "microphone", "camera", "photos", "location", "contacts", "calendar", "bluetooth", "motion", "biometric", "music", "health", "nfc"];
|
|
17
|
+
function iosPermissions(config) {
|
|
18
|
+
const enabled = {
|
|
19
|
+
speech: config.enableSpeechRecognition,
|
|
20
|
+
microphone: config.enableSpeechRecognition || config.enableAudioRecording || config.enableVideoRecording,
|
|
21
|
+
camera: config.enableCamera || config.enableVideoRecording || config.enableQRScanner || config.enableAR,
|
|
22
|
+
photos: config.enableCamera || config.enableVideoRecording,
|
|
23
|
+
location: config.enableGeolocation || config.enableBackgroundLocation,
|
|
24
|
+
contacts: config.enableContacts,
|
|
25
|
+
calendar: config.enableCalendar,
|
|
26
|
+
bluetooth: config.enableBluetooth || config.enableHeartRate,
|
|
27
|
+
motion: config.enableMotionSensors,
|
|
28
|
+
biometric: config.enableBiometric,
|
|
29
|
+
music: config.enableAppleMusic,
|
|
30
|
+
health: config.enableHealthKit,
|
|
31
|
+
nfc: config.enableNFC
|
|
32
|
+
};
|
|
33
|
+
return iosPermissionNames.filter((name) => enabled[name] === true);
|
|
34
|
+
}
|
|
35
|
+
function iosPermissionMask(config) {
|
|
36
|
+
return iosPermissions(config).reduce((mask, name) => mask | 1 << iosPermissionNames.indexOf(name), 0);
|
|
37
|
+
}
|
|
38
|
+
function renderPermissionSource(source, config) {
|
|
39
|
+
const enabled = new Set(iosPermissions(config));
|
|
40
|
+
return source.replace(/#if CRAFT_(?:PERMISSION_([A-Z]+)|APPLE_(MUSIC))\b/g, (_match, permission, music) => {
|
|
41
|
+
const name = music ? "music" : permission.toLowerCase();
|
|
42
|
+
if (!iosPermissionNames.includes(name))
|
|
43
|
+
throw new Error(`Unknown Swift permission gate: ${name}`);
|
|
44
|
+
return `#if ${enabled.has(name) ? "true" : "false"}`;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
var archives = ["libcraft-ios.a", "libcraft-ios-simulator-arm64.a", "libcraft-ios-simulator-x64.a"];
|
|
48
|
+
var digest = (path) => createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
49
|
+
function verifyRuntimeProfile(directory, config) {
|
|
50
|
+
if (!existsSync(join(directory, "ios-permissions.json")))
|
|
51
|
+
throw new Error("Unprofiled Zig runtime: use runtimeSource to build archives for this app");
|
|
52
|
+
const profile = JSON.parse(readFileSync(join(directory, "ios-permissions.json"), "utf8"));
|
|
53
|
+
if (profile.version !== 1 || profile.mask !== iosPermissionMask(config))
|
|
54
|
+
throw new Error("Zig runtime permission profile does not match craft.config.json; rebuild it for this app");
|
|
55
|
+
if (!existsSync(join(directory, archives[0])) || !archives.slice(1).some((name) => existsSync(join(directory, name))))
|
|
56
|
+
throw new Error("Zig runtime must include device and simulator archives");
|
|
57
|
+
for (const name of archives) {
|
|
58
|
+
if (!existsSync(join(directory, name)) && !profile.sha256?.[name])
|
|
59
|
+
continue;
|
|
60
|
+
if (!existsSync(join(directory, name)) || profile.sha256?.[name] !== digest(join(directory, name)))
|
|
61
|
+
throw new Error(`Zig runtime archive identity mismatch: ${name}; rebuild it for this app`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function writeInstalledProfile(directory, config, source) {
|
|
65
|
+
const paths = ["device/libcraft-ios.a", "simulator/libcraft-ios.a"];
|
|
66
|
+
writeFileSync(join(directory, "profile.json"), JSON.stringify({
|
|
67
|
+
version: 1,
|
|
68
|
+
mask: iosPermissionMask(config),
|
|
69
|
+
source,
|
|
70
|
+
sha256: Object.fromEntries(paths.map((path) => [path, digest(join(directory, path))]))
|
|
71
|
+
}, null, 2));
|
|
72
|
+
}
|
|
73
|
+
function verifyInstalledProfile(directory, config) {
|
|
74
|
+
if (!existsSync(join(directory, "profile.json")))
|
|
75
|
+
throw new Error("Unprofiled Zig runtime: regenerate this project with runtimeSource before omitting permission descriptions");
|
|
76
|
+
const profile = JSON.parse(readFileSync(join(directory, "profile.json"), "utf8"));
|
|
77
|
+
if (profile.version !== 1 || profile.mask !== iosPermissionMask(config))
|
|
78
|
+
throw new Error("Installed Zig runtime permission profile is stale; rebuild it for craft.config.json");
|
|
79
|
+
for (const path of ["device/libcraft-ios.a", "simulator/libcraft-ios.a"]) {
|
|
80
|
+
if (!existsSync(join(directory, path)) || profile.sha256?.[path] !== digest(join(directory, path)))
|
|
81
|
+
throw new Error(`Installed Zig runtime identity mismatch: ${path}; rebuild it for this app`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function withAppRuntime(source, config, use) {
|
|
85
|
+
const root = resolve(source);
|
|
86
|
+
const manifest = readFileSync(join(root, "build.zig.zon"), "utf8");
|
|
87
|
+
const version = /\.minimum_zig_version\s*=\s*"([^"]+)"/.exec(manifest)?.[1];
|
|
88
|
+
if (!version)
|
|
89
|
+
throw new Error("Craft Zig source must declare its pinned minimum_zig_version");
|
|
90
|
+
const zig = process.env.CRAFT_ZIG || "zig";
|
|
91
|
+
const actual = Bun.spawnSync([zig, "version"], { stdout: "pipe", stderr: "pipe" });
|
|
92
|
+
if (actual.exitCode !== 0 || actual.stdout.toString().trim() !== version)
|
|
93
|
+
throw new Error(`Per-app iOS runtime requires Zig ${version}; set CRAFT_ZIG to that executable`);
|
|
94
|
+
const output = mkdtempSync(join(tmpdir(), "craft-ios-runtime-"));
|
|
95
|
+
try {
|
|
96
|
+
const names = iosPermissions(config);
|
|
97
|
+
const command = [zig, "build", "build-ios-all", `-Dios-permissions=${names.join(",") || "none"}`, "-Djs-runtime=false", "-Doptimize=ReleaseFast", "--prefix", output];
|
|
98
|
+
const child = Bun.spawn(command, { cwd: root, stdout: "inherit", stderr: "inherit" });
|
|
99
|
+
if (await child.exited !== 0)
|
|
100
|
+
throw new Error("Per-app iOS Zig runtime build failed; installed runtime was not replaced");
|
|
101
|
+
const directory = join(output, "lib");
|
|
102
|
+
const profile = JSON.parse(readFileSync(join(directory, "ios-permissions.json"), "utf8"));
|
|
103
|
+
if (profile.version !== 1 || profile.mask !== iosPermissionMask(config))
|
|
104
|
+
throw new Error("Craft Zig source did not build the requested iOS permission profile");
|
|
105
|
+
profile.sha256 = Object.fromEntries(archives.map((name) => [name, digest(join(directory, name))]));
|
|
106
|
+
writeFileSync(join(directory, "ios-permissions.json"), `${JSON.stringify(profile, null, 2)}
|
|
107
|
+
`);
|
|
108
|
+
verifyRuntimeProfile(directory, config);
|
|
109
|
+
return await use(directory);
|
|
110
|
+
} finally {
|
|
111
|
+
rmSync(output, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/index.ts
|
|
116
|
+
import { cpSync, existsSync as existsSync2, mkdirSync, mkdtempSync as mkdtempSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, renameSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
117
|
+
import { dirname as dirname2, extname, join as join4, resolve as resolve2, sep as sep2 } from "path";
|
|
12
118
|
var {$ } = globalThis.Bun;
|
|
13
119
|
|
|
14
120
|
// src/app-shortcuts.ts
|
|
@@ -67,7 +173,7 @@ ${entries}
|
|
|
67
173
|
|
|
68
174
|
// src/source-audit.ts
|
|
69
175
|
import { readdirSync } from "fs";
|
|
70
|
-
import { join, relative, sep } from "path";
|
|
176
|
+
import { join as join2, relative, sep } from "path";
|
|
71
177
|
var defaultIgnoredPaths = [
|
|
72
178
|
".git",
|
|
73
179
|
"node_modules",
|
|
@@ -87,7 +193,7 @@ function isIgnored(path, ignoredPaths) {
|
|
|
87
193
|
}
|
|
88
194
|
function visit(root, current, ignoredPaths, found) {
|
|
89
195
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
90
|
-
const absolute =
|
|
196
|
+
const absolute = join2(current, entry.name);
|
|
91
197
|
const relativePath = relative(root, absolute);
|
|
92
198
|
if (isIgnored(relativePath, ignoredPaths))
|
|
93
199
|
continue;
|
|
@@ -114,27 +220,27 @@ ${sources.map((path) => `- ${path}`).join(`
|
|
|
114
220
|
}
|
|
115
221
|
}
|
|
116
222
|
// src/testing.ts
|
|
117
|
-
import { dirname, join as
|
|
118
|
-
var templates =
|
|
223
|
+
import { dirname, join as join3 } from "path";
|
|
224
|
+
var templates = join3(dirname(import.meta.dir), "templates/testing");
|
|
119
225
|
|
|
120
226
|
// src/index.ts
|
|
121
227
|
function compressNativeSources(target) {
|
|
122
228
|
if (statSync(target).isDirectory()) {
|
|
123
229
|
for (const entry of readdirSync2(target))
|
|
124
|
-
compressNativeSources(
|
|
230
|
+
compressNativeSources(join4(target, entry));
|
|
125
231
|
return;
|
|
126
232
|
}
|
|
127
233
|
if (!/\.(?:js|json)$/.test(target))
|
|
128
234
|
return;
|
|
129
|
-
const bytes =
|
|
235
|
+
const bytes = readFileSync2(target);
|
|
130
236
|
if (bytes.length < 1024 || bytes[0] === 31 && bytes[1] === 139)
|
|
131
237
|
return;
|
|
132
238
|
const packed = gzipSync(bytes, { level: 9 });
|
|
133
239
|
if (packed.length < bytes.length)
|
|
134
|
-
|
|
240
|
+
writeFileSync2(target, packed);
|
|
135
241
|
}
|
|
136
242
|
function syncNativeFonts(fonts, output) {
|
|
137
|
-
const destination =
|
|
243
|
+
const destination = join4(output, "NativeFonts");
|
|
138
244
|
const names = new Set;
|
|
139
245
|
const files = Object.entries(fonts ?? {}).map(([family, entry]) => {
|
|
140
246
|
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(family))
|
|
@@ -142,32 +248,32 @@ function syncNativeFonts(fonts, output) {
|
|
|
142
248
|
if (names.has(family.toLowerCase()))
|
|
143
249
|
throw new Error(`Duplicate native font name: ${family}`);
|
|
144
250
|
names.add(family.toLowerCase());
|
|
145
|
-
if ([entry.src, entry.license].some((file) => file &&
|
|
251
|
+
if ([entry.src, entry.license].some((file) => file && resolve2(file).startsWith(resolve2(destination) + sep2)))
|
|
146
252
|
throw new Error("Native font sources must be outside the generated NativeFonts directory");
|
|
147
253
|
const extension = extname(entry.src).toLowerCase();
|
|
148
|
-
if (![".ttf", ".otf"].includes(extension) || !
|
|
254
|
+
if (![".ttf", ".otf"].includes(extension) || !existsSync2(entry.src) || !statSync(entry.src).isFile())
|
|
149
255
|
throw new Error(`Native font ${family} must reference an existing TTF or OTF file: ${entry.src}`);
|
|
150
|
-
if (entry.license && (!
|
|
256
|
+
if (entry.license && (!existsSync2(entry.license) || !statSync(entry.license).isFile()))
|
|
151
257
|
throw new Error(`Native font license not found: ${entry.license}`);
|
|
152
258
|
return { family, ...entry, filename: `${family}${extension}` };
|
|
153
259
|
});
|
|
154
|
-
|
|
260
|
+
rmSync2(destination, { recursive: true, force: true });
|
|
155
261
|
if (!files.length)
|
|
156
262
|
return [];
|
|
157
263
|
mkdirSync(destination, { recursive: true });
|
|
158
264
|
for (const file of files) {
|
|
159
|
-
cpSync(file.src,
|
|
265
|
+
cpSync(file.src, join4(destination, file.filename));
|
|
160
266
|
if (file.license)
|
|
161
|
-
cpSync(file.license,
|
|
267
|
+
cpSync(file.license, join4(destination, `${file.family}.LICENSE.txt`));
|
|
162
268
|
}
|
|
163
269
|
return files.map((file) => `NativeFonts/${file.filename}`);
|
|
164
270
|
}
|
|
165
|
-
var TEMPLATES_DIR =
|
|
271
|
+
var TEMPLATES_DIR = join4(dirname2(import.meta.dir), "templates");
|
|
166
272
|
function resolveCraftVersion(startDir = import.meta.dir) {
|
|
167
|
-
let directory =
|
|
273
|
+
let directory = resolve2(startDir);
|
|
168
274
|
while (true) {
|
|
169
275
|
try {
|
|
170
|
-
const packageJSON = JSON.parse(
|
|
276
|
+
const packageJSON = JSON.parse(readFileSync2(join4(directory, "package.json"), "utf8"));
|
|
171
277
|
if ((packageJSON.name === "@craft-native/ios" || packageJSON.name === "craft-native") && typeof packageJSON.version === "string" && packageJSON.version.length > 0)
|
|
172
278
|
return packageJSON.version;
|
|
173
279
|
} catch {}
|
|
@@ -319,29 +425,29 @@ function hybridConfigProblems(config) {
|
|
|
319
425
|
return problems;
|
|
320
426
|
}
|
|
321
427
|
function installNativeBundle(source, output) {
|
|
322
|
-
if (!
|
|
428
|
+
if (!existsSync2(source))
|
|
323
429
|
throw new Error(`Native bundle not found: ${source}`);
|
|
324
|
-
const single =
|
|
325
|
-
const routes =
|
|
430
|
+
const single = join4(output, "dist", "native-screen.js");
|
|
431
|
+
const routes = join4(output, "dist", "native-screens");
|
|
326
432
|
mkdirSync(dirname2(single), { recursive: true });
|
|
327
433
|
if (statSync(source).isDirectory()) {
|
|
328
434
|
if (!readdirSync2(source).some((name) => name.endsWith(".js")))
|
|
329
435
|
throw new Error(`Native route bundles not found in ${source}`);
|
|
330
|
-
if (
|
|
331
|
-
|
|
436
|
+
if (resolve2(source) !== resolve2(routes)) {
|
|
437
|
+
rmSync2(routes, { recursive: true, force: true });
|
|
332
438
|
cpSync(source, routes, { recursive: true });
|
|
333
439
|
}
|
|
334
440
|
compressNativeSources(routes);
|
|
335
|
-
|
|
441
|
+
rmSync2(single, { force: true });
|
|
336
442
|
return;
|
|
337
443
|
}
|
|
338
|
-
if (
|
|
444
|
+
if (resolve2(source) !== resolve2(single))
|
|
339
445
|
cpSync(source, single);
|
|
340
446
|
compressNativeSources(single);
|
|
341
|
-
|
|
447
|
+
rmSync2(routes, { recursive: true, force: true });
|
|
342
448
|
}
|
|
343
449
|
function hasInstalledNativeBundle(output) {
|
|
344
|
-
return
|
|
450
|
+
return existsSync2(join4(output, "dist", "native-screen.js")) || existsSync2(join4(output, "dist", "native-screens"));
|
|
345
451
|
}
|
|
346
452
|
function runPostGenerateHook(hook, output, phase = "build") {
|
|
347
453
|
if (!hook)
|
|
@@ -447,23 +553,10 @@ function renderAppearance(config) {
|
|
|
447
553
|
return { interfaceStyle: "Light", statusBarStyle: "UIStatusBarStyleDarkContent" };
|
|
448
554
|
return { interfaceStyle: "Dark", statusBarStyle: "UIStatusBarStyleLightContent" };
|
|
449
555
|
}
|
|
450
|
-
var LINKED_PURPOSES = {
|
|
451
|
-
NSSpeechRecognitionUsageDescription: "speech recognition",
|
|
452
|
-
NSMicrophoneUsageDescription: "the microphone",
|
|
453
|
-
NSCameraUsageDescription: "the camera",
|
|
454
|
-
NSPhotoLibraryUsageDescription: "your photo library",
|
|
455
|
-
NSLocationWhenInUseUsageDescription: "your location",
|
|
456
|
-
NSContactsUsageDescription: "your contacts",
|
|
457
|
-
NSCalendarsUsageDescription: "your calendar",
|
|
458
|
-
NSBluetoothAlwaysUsageDescription: "Bluetooth",
|
|
459
|
-
NSMotionUsageDescription: "motion data",
|
|
460
|
-
NSFaceIDUsageDescription: "Face ID",
|
|
461
|
-
NSAppleMusicUsageDescription: "your music"
|
|
462
|
-
};
|
|
463
556
|
function renderUsageDescriptions(config) {
|
|
464
557
|
const entries = [
|
|
465
558
|
[config.enableSpeechRecognition, "NSSpeechRecognitionUsageDescription", `${config.appName} uses speech recognition for voice input.`],
|
|
466
|
-
[config.enableSpeechRecognition || config.enableAudioRecording, "NSMicrophoneUsageDescription", `${config.appName} uses the microphone to record audio.`],
|
|
559
|
+
[config.enableSpeechRecognition || config.enableAudioRecording || config.enableVideoRecording, "NSMicrophoneUsageDescription", `${config.appName} uses the microphone to record audio.`],
|
|
467
560
|
[config.enableCamera || config.enableVideoRecording || config.enableQRScanner || config.enableAR, "NSCameraUsageDescription", `${config.appName} uses the camera for photos, video, scanning, and augmented reality.`],
|
|
468
561
|
[config.enableCamera || config.enableVideoRecording, "NSPhotoLibraryUsageDescription", `${config.appName} lets you choose photos and videos from your library.`],
|
|
469
562
|
[config.enableGeolocation || config.enableBackgroundLocation, "NSLocationWhenInUseUsageDescription", `${config.appName} uses your location while you record an activity.`],
|
|
@@ -486,8 +579,7 @@ function renderUsageDescriptions(config) {
|
|
|
486
579
|
return [key, custom];
|
|
487
580
|
if (enabled)
|
|
488
581
|
return [key, value];
|
|
489
|
-
|
|
490
|
-
return linked ? [key, `${config.appName} asks for ${linked} only in a feature that needs it.`] : null;
|
|
582
|
+
return null;
|
|
491
583
|
}).filter((entry) => entry !== null).map(([key, value]) => plistString(key, value)).join(`
|
|
492
584
|
`);
|
|
493
585
|
}
|
|
@@ -652,9 +744,11 @@ function renderRuntimeSettings() {
|
|
|
652
744
|
`);
|
|
653
745
|
}
|
|
654
746
|
function renderFeatureSettings(config) {
|
|
655
|
-
const conditions =
|
|
747
|
+
const conditions = iosPermissions(config).filter((name) => name !== "music").map((name) => `CRAFT_PERMISSION_${name.toUpperCase()}`);
|
|
656
748
|
if (config.enableScheduledWorkouts)
|
|
657
749
|
conditions.push("CRAFT_SCHEDULED_WORKOUTS");
|
|
750
|
+
if (config.enableAppleMusic)
|
|
751
|
+
conditions.push("CRAFT_APPLE_MUSIC");
|
|
658
752
|
if (!conditions.length)
|
|
659
753
|
return "";
|
|
660
754
|
return ` SWIFT_ACTIVE_COMPILATION_CONDITIONS: "$(inherited) ${conditions.join(" ")}"`;
|
|
@@ -729,7 +823,7 @@ function pngHasAlpha(bytes) {
|
|
|
729
823
|
return false;
|
|
730
824
|
}
|
|
731
825
|
function writeOpaqueIcon(source, target) {
|
|
732
|
-
const bytes = new Uint8Array(
|
|
826
|
+
const bytes = new Uint8Array(readFileSync2(source));
|
|
733
827
|
if (!pngHasAlpha(bytes) || process.platform !== "darwin") {
|
|
734
828
|
cpSync(source, target);
|
|
735
829
|
return;
|
|
@@ -738,27 +832,27 @@ function writeOpaqueIcon(source, target) {
|
|
|
738
832
|
const run = (args) => Bun.spawnSync(["sips", ...args], { stdout: "ignore", stderr: "pipe" });
|
|
739
833
|
const toJpeg = run(["-s", "format", "jpeg", "-s", "formatOptions", "100", source, "--out", flattened]);
|
|
740
834
|
const toPng = toJpeg.exitCode === 0 ? run(["-s", "format", "png", flattened, "--out", target]) : toJpeg;
|
|
741
|
-
|
|
835
|
+
rmSync2(flattened, { force: true });
|
|
742
836
|
if (toPng.exitCode !== 0) {
|
|
743
837
|
cpSync(source, target);
|
|
744
838
|
console.warn(`[craft] Could not remove the app icon's alpha channel; App Store Connect will refuse it: ${toPng.stderr.toString().trim()}`);
|
|
745
839
|
}
|
|
746
840
|
}
|
|
747
841
|
function renderAssetCatalog(output, config) {
|
|
748
|
-
const catalog =
|
|
749
|
-
const appIcon =
|
|
750
|
-
const launchBackground =
|
|
842
|
+
const catalog = join4(output, "Assets.xcassets");
|
|
843
|
+
const appIcon = join4(catalog, "AppIcon.appiconset");
|
|
844
|
+
const launchBackground = join4(catalog, "LaunchBackground.colorset");
|
|
751
845
|
mkdirSync(appIcon, { recursive: true });
|
|
752
846
|
mkdirSync(launchBackground, { recursive: true });
|
|
753
|
-
|
|
847
|
+
writeFileSync2(join4(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
|
|
754
848
|
`);
|
|
755
849
|
const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
|
|
756
850
|
if (config.appIconPath) {
|
|
757
|
-
if (!
|
|
851
|
+
if (!existsSync2(config.appIconPath))
|
|
758
852
|
throw new Error(`App icon not found: ${config.appIconPath}`);
|
|
759
|
-
writeOpaqueIcon(config.appIconPath,
|
|
853
|
+
writeOpaqueIcon(config.appIconPath, join4(appIcon, iconFilename));
|
|
760
854
|
}
|
|
761
|
-
|
|
855
|
+
writeFileSync2(join4(appIcon, "Contents.json"), `${JSON.stringify({
|
|
762
856
|
images: iconFilename ? [{
|
|
763
857
|
filename: iconFilename,
|
|
764
858
|
idiom: "universal",
|
|
@@ -785,7 +879,7 @@ function renderAssetCatalog(output, config) {
|
|
|
785
879
|
idiom: "universal"
|
|
786
880
|
});
|
|
787
881
|
}
|
|
788
|
-
|
|
882
|
+
writeFileSync2(join4(launchBackground, "Contents.json"), `${JSON.stringify({
|
|
789
883
|
colors,
|
|
790
884
|
info: { author: "xcode", version: 1 }
|
|
791
885
|
}, null, 2)}
|
|
@@ -794,8 +888,8 @@ function renderAssetCatalog(output, config) {
|
|
|
794
888
|
}
|
|
795
889
|
var LAUNCH_LOGO = "LaunchLogo";
|
|
796
890
|
function renderLaunchLogo(catalog, config) {
|
|
797
|
-
const imageset =
|
|
798
|
-
|
|
891
|
+
const imageset = join4(catalog, `${LAUNCH_LOGO}.imageset`);
|
|
892
|
+
rmSync2(imageset, { recursive: true, force: true });
|
|
799
893
|
if (!config.splashImagePath)
|
|
800
894
|
return;
|
|
801
895
|
mkdirSync(imageset, { recursive: true });
|
|
@@ -804,20 +898,20 @@ function renderLaunchLogo(catalog, config) {
|
|
|
804
898
|
for (const [source, dark] of [[config.splashImagePath, false], [config.splashImagePathDark, true]]) {
|
|
805
899
|
if (!source)
|
|
806
900
|
continue;
|
|
807
|
-
if (!
|
|
901
|
+
if (!existsSync2(source))
|
|
808
902
|
throw new Error(`Splash image not found: ${source}`);
|
|
809
903
|
const extension = source.split(".").pop().toLowerCase();
|
|
810
904
|
if (extension === "svg" || extension === "pdf")
|
|
811
905
|
vector = true;
|
|
812
906
|
const filename = `${LAUNCH_LOGO}${dark ? "-dark" : ""}.${extension}`;
|
|
813
|
-
cpSync(source,
|
|
907
|
+
cpSync(source, join4(imageset, filename));
|
|
814
908
|
images.push({
|
|
815
909
|
...dark ? { appearances: [{ appearance: "luminosity", value: "dark" }] } : {},
|
|
816
910
|
filename,
|
|
817
911
|
idiom: "universal"
|
|
818
912
|
});
|
|
819
913
|
}
|
|
820
|
-
|
|
914
|
+
writeFileSync2(join4(imageset, "Contents.json"), `${JSON.stringify({
|
|
821
915
|
images,
|
|
822
916
|
info: { author: "xcode", version: 1 },
|
|
823
917
|
...vector ? { properties: { "preserves-vector-representation": true } } : {}
|
|
@@ -928,18 +1022,18 @@ function renderOfflinePage(config) {
|
|
|
928
1022
|
`;
|
|
929
1023
|
}
|
|
930
1024
|
function syncWebAssets(source, output) {
|
|
931
|
-
const sourcePath =
|
|
932
|
-
if (!
|
|
1025
|
+
const sourcePath = resolve2(source);
|
|
1026
|
+
if (!existsSync2(sourcePath))
|
|
933
1027
|
throw new Error(`Web asset path not found: ${source}`);
|
|
934
|
-
const distDir =
|
|
935
|
-
|
|
1028
|
+
const distDir = join4(output, "dist");
|
|
1029
|
+
rmSync2(distDir, { recursive: true, force: true });
|
|
936
1030
|
mkdirSync(distDir, { recursive: true });
|
|
937
1031
|
if (statSync(sourcePath).isDirectory()) {
|
|
938
1032
|
cpSync(sourcePath, distDir, { recursive: true });
|
|
939
1033
|
} else {
|
|
940
|
-
cpSync(sourcePath,
|
|
1034
|
+
cpSync(sourcePath, join4(distDir, "index.html"));
|
|
941
1035
|
}
|
|
942
|
-
if (!
|
|
1036
|
+
if (!existsSync2(join4(distDir, "index.html"))) {
|
|
943
1037
|
throw new Error(`Web asset directory must contain index.html: ${source}`);
|
|
944
1038
|
}
|
|
945
1039
|
}
|
|
@@ -949,7 +1043,7 @@ function resolveRuntimeDir(override) {
|
|
|
949
1043
|
const dir = override ?? process.env.CRAFT_IOS_RUNTIME;
|
|
950
1044
|
if (!dir)
|
|
951
1045
|
return null;
|
|
952
|
-
if (!
|
|
1046
|
+
if (!existsSync2(dir)) {
|
|
953
1047
|
const source = override === undefined ? "CRAFT_IOS_RUNTIME points at" : "runtimeDir is";
|
|
954
1048
|
throw new Error(`${source} ${dir}, which does not exist.`);
|
|
955
1049
|
}
|
|
@@ -959,43 +1053,71 @@ var RUNTIME_ARCHIVES = {
|
|
|
959
1053
|
device: ["libcraft-ios.a"],
|
|
960
1054
|
simulator: ["libcraft-ios-simulator-arm64.a", "libcraft-ios-simulator-x64.a"]
|
|
961
1055
|
};
|
|
962
|
-
async function installRuntime(output, runtimeDir) {
|
|
1056
|
+
async function installRuntime(output, runtimeDir, config) {
|
|
1057
|
+
if (config)
|
|
1058
|
+
verifyRuntimeProfile(runtimeDir, config);
|
|
963
1059
|
const resolved = Object.entries(RUNTIME_ARCHIVES).map(([sdk, archives]) => {
|
|
964
|
-
const present = archives.filter((a) =>
|
|
1060
|
+
const present = archives.filter((a) => existsSync2(join4(runtimeDir, a)));
|
|
965
1061
|
if (present.length === 0) {
|
|
966
1062
|
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.`);
|
|
967
1063
|
}
|
|
968
1064
|
return { sdk, archives, present };
|
|
969
1065
|
});
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1066
|
+
const installed = join4(output, "Runtime");
|
|
1067
|
+
const staging = mkdtempSync2(join4(output, ".craft-runtime-"));
|
|
1068
|
+
const dest = join4(staging, "next");
|
|
1069
|
+
const previous = join4(staging, "previous");
|
|
1070
|
+
try {
|
|
1071
|
+
for (const { sdk, archives, present } of resolved) {
|
|
1072
|
+
const sdkDir = join4(dest, sdk);
|
|
1073
|
+
mkdirSync(sdkDir, { recursive: true });
|
|
1074
|
+
const target = join4(sdkDir, "libcraft-ios.a");
|
|
1075
|
+
if (present.length === 1) {
|
|
1076
|
+
const missing = archives.filter((a) => !present.includes(a));
|
|
1077
|
+
if (missing.length > 0) {
|
|
1078
|
+
console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
|
|
1079
|
+
}
|
|
1080
|
+
cpSync(join4(runtimeDir, present[0]), target);
|
|
1081
|
+
} else {
|
|
1082
|
+
await $`lipo -create ${present.map((a) => join4(runtimeDir, a))} -output ${target}`.quiet();
|
|
980
1083
|
}
|
|
981
|
-
cpSync(join3(runtimeDir, present[0]), target);
|
|
982
|
-
} else {
|
|
983
|
-
await $`lipo -create ${present.map((a) => join3(runtimeDir, a))} -output ${target}`.quiet();
|
|
984
1084
|
}
|
|
1085
|
+
if (config)
|
|
1086
|
+
writeInstalledProfile(dest, config);
|
|
1087
|
+
if (existsSync2(installed))
|
|
1088
|
+
renameSync(installed, previous);
|
|
1089
|
+
try {
|
|
1090
|
+
renameSync(dest, installed);
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
if (existsSync2(previous))
|
|
1093
|
+
renameSync(previous, installed);
|
|
1094
|
+
throw error;
|
|
1095
|
+
}
|
|
1096
|
+
return true;
|
|
1097
|
+
} finally {
|
|
1098
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
985
1099
|
}
|
|
986
|
-
return true;
|
|
987
1100
|
}
|
|
988
|
-
async function refreshRuntime(output, override) {
|
|
989
|
-
if (!
|
|
1101
|
+
async function refreshRuntime(output, config, override) {
|
|
1102
|
+
if (!existsSync2(join4(output, "Runtime")))
|
|
990
1103
|
return;
|
|
991
1104
|
const dir = resolveRuntimeDir(override);
|
|
992
1105
|
if (!dir) {
|
|
1106
|
+
verifyInstalledProfile(join4(output, "Runtime"), config);
|
|
993
1107
|
console.log(" Keeping the Zig runtime installed at init (no runtime directory configured)");
|
|
994
1108
|
return;
|
|
995
1109
|
}
|
|
996
|
-
await installRuntime(output, dir);
|
|
1110
|
+
await installRuntime(output, dir, config);
|
|
997
1111
|
console.log(" Refreshed the Zig runtime from", dir);
|
|
998
1112
|
}
|
|
1113
|
+
async function installAppRuntime(output, source, config) {
|
|
1114
|
+
return withAppRuntime(source, config, async (directory) => {
|
|
1115
|
+
verifyRuntimeProfile(directory, config);
|
|
1116
|
+
await installRuntime(output, directory, config);
|
|
1117
|
+
writeInstalledProfile(join4(output, "Runtime"), config, resolve2(source));
|
|
1118
|
+
return true;
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
999
1121
|
function productName(name) {
|
|
1000
1122
|
const words = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
1001
1123
|
const joined = words.map((word) => word[0].toUpperCase() + word.slice(1)).join("");
|
|
@@ -1006,45 +1128,63 @@ function productName(name) {
|
|
|
1006
1128
|
function writeShell(output, config) {
|
|
1007
1129
|
if (!config.appName || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(config.bundleId))
|
|
1008
1130
|
throw new Error("The iOS shell needs an appName and a valid bundleId in craft.config.json.");
|
|
1131
|
+
const writeSource = (path, source) => {
|
|
1132
|
+
const rendered = renderPermissionSource(source, config);
|
|
1133
|
+
if (!existsSync2(path) || readFileSync2(path, "utf8") !== rendered)
|
|
1134
|
+
writeFileSync2(path, rendered);
|
|
1135
|
+
};
|
|
1009
1136
|
const name = productName(config.appName);
|
|
1010
1137
|
const shortcutSource = appShortcutsSwift(config.appShortcuts, config.urlSchemes);
|
|
1011
|
-
mkdirSync(
|
|
1012
|
-
mkdirSync(
|
|
1013
|
-
const swiftTemplate =
|
|
1138
|
+
mkdirSync(join4(output, "Sources"), { recursive: true });
|
|
1139
|
+
mkdirSync(join4(output, "Shared"), { recursive: true });
|
|
1140
|
+
const swiftTemplate = readFileSync2(join4(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
|
|
1014
1141
|
const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, config.bundleId).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION);
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1142
|
+
writeSource(join4(output, "Sources", `${name}App.swift`), swiftSource);
|
|
1143
|
+
writeSource(join4(output, "Sources", "CraftAppShortcuts.swift"), shortcutSource);
|
|
1144
|
+
writeSource(join4(output, "Sources", "CraftNativeScreen.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftNativeScreen.swift"), "utf8"));
|
|
1145
|
+
writeSource(join4(output, "Sources", "CraftNativeFlatList.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftNativeFlatList.swift"), "utf8"));
|
|
1146
|
+
writeSource(join4(output, "Sources", "CraftNativeActions.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftNativeActions.swift"), "utf8"));
|
|
1147
|
+
writeSource(join4(output, "Sources", "CraftHealth.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftHealth.swift"), "utf8"));
|
|
1148
|
+
writeSource(join4(output, "Sources", "CraftMusic.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftMusic.swift"), "utf8"));
|
|
1149
|
+
writeSource(join4(output, "Sources", "CraftBluetooth.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftBluetooth.swift"), "utf8"));
|
|
1150
|
+
writeSource(join4(output, "Sources", "CraftHeartRate.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftHeartRate.swift"), "utf8"));
|
|
1151
|
+
writeSource(join4(output, "Sources", "CraftRecordingArchive.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftRecordingArchive.swift"), "utf8"));
|
|
1152
|
+
writeSource(join4(output, "Sources", "CraftPersistentStorage.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftPersistentStorage.swift"), "utf8"));
|
|
1153
|
+
writeSource(join4(output, "Sources", "CraftScheduledWorkouts.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftScheduledWorkouts.swift"), "utf8"));
|
|
1154
|
+
writeSource(join4(output, "Sources", "CraftNativeMutation.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftNativeMutation.swift"), "utf8"));
|
|
1155
|
+
writeSource(join4(output, "Sources", "CraftNativeUI.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftNativeUI.swift"), "utf8"));
|
|
1156
|
+
writeSource(join4(output, "Sources", "CraftNativeServices.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftNativeServices.swift"), "utf8"));
|
|
1157
|
+
writeSource(join4(output, "Sources", "CraftNativeFiles.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftNativeFiles.swift"), "utf8"));
|
|
1158
|
+
writeSource(join4(output, "Sources", "CraftVoiceRecording.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftVoiceRecording.swift"), "utf8"));
|
|
1159
|
+
writeSource(join4(output, "Sources", "CraftWebAudio.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftWebAudio.swift"), "utf8"));
|
|
1160
|
+
writeSource(join4(output, "Sources", "CraftHybrid.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftHybrid.swift"), "utf8"));
|
|
1161
|
+
writeSource(join4(output, "Sources", "CraftCrashReporter.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftCrashReporter.swift"), "utf-8").replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION));
|
|
1162
|
+
writeSource(join4(output, "Sources", "CraftLiveActivities.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftLiveActivities.swift"), "utf8"));
|
|
1163
|
+
writeSource(join4(output, "Shared", "CraftActivityAttributes.swift"), readFileSync2(join4(TEMPLATES_DIR, "CraftActivityAttributes.swift"), "utf8"));
|
|
1037
1164
|
for (const [enabled, folder, template, filename] of [
|
|
1038
1165
|
[config.enableLiveActivities, "WidgetExtension", "CraftLiveActivityWidget.swift.template", `${name}LiveActivity.swift`],
|
|
1039
1166
|
[config.enableWatchApp, "WatchExtension", "CraftWatchApp.swift.template", `${name}WatchApp.swift`]
|
|
1040
1167
|
]) {
|
|
1041
1168
|
if (!enabled)
|
|
1042
1169
|
continue;
|
|
1043
|
-
mkdirSync(
|
|
1044
|
-
const source =
|
|
1045
|
-
|
|
1170
|
+
mkdirSync(join4(output, folder), { recursive: true });
|
|
1171
|
+
const source = readFileSync2(join4(TEMPLATES_DIR, template), "utf8").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{HEALTH_KIT_ENABLED\}\}/g, config.enableHealthKit ? "true" : "false");
|
|
1172
|
+
writeSource(join4(output, folder, filename), source);
|
|
1046
1173
|
}
|
|
1047
1174
|
}
|
|
1175
|
+
function refreshUsageDescriptions(output, config) {
|
|
1176
|
+
const path = join4(output, "Info.plist");
|
|
1177
|
+
if (!existsSync2(path))
|
|
1178
|
+
return;
|
|
1179
|
+
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;
|
|
1180
|
+
const plist = readFileSync2(path, "utf8").replace(managed, "");
|
|
1181
|
+
const end = plist.lastIndexOf("</dict>");
|
|
1182
|
+
if (end < 0)
|
|
1183
|
+
throw new Error("Info.plist must contain a root dictionary");
|
|
1184
|
+
writeFileSync2(path, `${plist.slice(0, end).trimEnd()}
|
|
1185
|
+
${renderUsageDescriptions(config)}
|
|
1186
|
+
${plist.slice(end)}`);
|
|
1187
|
+
}
|
|
1048
1188
|
async function init(options) {
|
|
1049
1189
|
const { name: displayName, bundleId, teamId, output } = options;
|
|
1050
1190
|
const name = productName(displayName);
|
|
@@ -1052,11 +1192,11 @@ async function init(options) {
|
|
|
1052
1192
|
\u26A1 Initializing Craft iOS project: ${displayName}`);
|
|
1053
1193
|
console.log(` Output: ${output}
|
|
1054
1194
|
`);
|
|
1055
|
-
const dirs = [output,
|
|
1195
|
+
const dirs = [output, join4(output, "Sources"), join4(output, "Shared"), join4(output, "dist")];
|
|
1056
1196
|
if (options.config?.enableWatchApp)
|
|
1057
|
-
dirs.push(
|
|
1197
|
+
dirs.push(join4(output, "WatchApp"), join4(output, "WatchExtension"));
|
|
1058
1198
|
for (const dir of dirs) {
|
|
1059
|
-
if (!
|
|
1199
|
+
if (!existsSync2(dir)) {
|
|
1060
1200
|
mkdirSync(dir, { recursive: true });
|
|
1061
1201
|
}
|
|
1062
1202
|
}
|
|
@@ -1086,10 +1226,20 @@ async function init(options) {
|
|
|
1086
1226
|
`));
|
|
1087
1227
|
if (Object.keys(config.shareStorage ?? {}).length)
|
|
1088
1228
|
config.enableSecureStorage = true;
|
|
1089
|
-
|
|
1229
|
+
const runtimeDir = resolveRuntimeDir(options.runtimeDir);
|
|
1230
|
+
const runtimeSource = options.runtimeSource === null || options.runtimeDir === null ? undefined : options.runtimeSource ?? process.env.CRAFT_IOS_RUNTIME_SOURCE;
|
|
1231
|
+
if (runtimeSource && runtimeDir)
|
|
1232
|
+
throw new Error("Choose runtimeSource or runtimeDir, not both");
|
|
1233
|
+
const hasRuntime = runtimeSource ? await installAppRuntime(output, runtimeSource, config) : runtimeDir ? await installRuntime(output, runtimeDir, config) : false;
|
|
1234
|
+
if (hasRuntime) {
|
|
1235
|
+
console.log(" Linked the Zig runtime from", runtimeSource ?? runtimeDir);
|
|
1236
|
+
} else {
|
|
1237
|
+
rmSync2(join4(output, "Runtime"), { recursive: true, force: true });
|
|
1238
|
+
}
|
|
1239
|
+
writeFileSync2(join4(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
1090
1240
|
writeShell(output, config);
|
|
1091
1241
|
const nativeFontFiles = syncNativeFonts(config.nativeFonts, output);
|
|
1092
|
-
const infoPlistTemplate =
|
|
1242
|
+
const infoPlistTemplate = readFileSync2(join4(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
|
|
1093
1243
|
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>
|
|
1094
1244
|
<array>
|
|
1095
1245
|
${plistArray(nativeFontFiles, 2)}
|
|
@@ -1097,8 +1247,8 @@ ${plistArray(nativeFontFiles, 2)}
|
|
|
1097
1247
|
<true/>
|
|
1098
1248
|
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
|
|
1099
1249
|
<true/>` : "");
|
|
1100
|
-
|
|
1101
|
-
const projectYmlTemplate =
|
|
1250
|
+
writeFileSync2(join4(output, "Info.plist"), infoPlist);
|
|
1251
|
+
const projectYmlTemplate = readFileSync2(join4(TEMPLATES_DIR, "project.yml.template"), "utf-8");
|
|
1102
1252
|
const nativeDependencies = [];
|
|
1103
1253
|
if (config.enableLiveActivities)
|
|
1104
1254
|
nativeDependencies.push(` - target: ${name}LiveActivity`);
|
|
@@ -1153,38 +1303,31 @@ ${plistArray(nativeFontFiles, 2)}
|
|
|
1153
1303
|
APPLICATION_EXTENSION_API_ONLY: YES
|
|
1154
1304
|
SKIP_INSTALL: YES`);
|
|
1155
1305
|
}
|
|
1156
|
-
const runtimeDir = resolveRuntimeDir(options.runtimeDir);
|
|
1157
|
-
const hasRuntime = runtimeDir ? await installRuntime(output, runtimeDir) : false;
|
|
1158
|
-
if (hasRuntime) {
|
|
1159
|
-
console.log(" Linked the Zig runtime from", runtimeDir);
|
|
1160
|
-
} else {
|
|
1161
|
-
rmSync(join3(output, "Runtime"), { recursive: true, force: true });
|
|
1162
|
-
}
|
|
1163
1306
|
const projectYml = projectYmlTemplate.replace(/\{\{NATIVE_FONT_RESOURCES\}\}/g, nativeFontFiles.length ? ` - path: NativeFonts
|
|
1164
1307
|
type: folder
|
|
1165
1308
|
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:
|
|
1166
1309
|
${nativeDependencies.join(`
|
|
1167
1310
|
`)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
|
|
1168
1311
|
`));
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1312
|
+
writeFileSync2(join4(output, "project.yml"), projectYml);
|
|
1313
|
+
writeFileSync2(join4(output, "Craft.entitlements"), renderEntitlements(config));
|
|
1314
|
+
writeFileSync2(join4(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
|
|
1172
1315
|
renderAssetCatalog(output, config);
|
|
1173
1316
|
if (config.enableLiveActivities) {
|
|
1174
|
-
mkdirSync(
|
|
1175
|
-
const widgetInfo =
|
|
1176
|
-
|
|
1317
|
+
mkdirSync(join4(output, "WidgetExtension"), { recursive: true });
|
|
1318
|
+
const widgetInfo = readFileSync2(join4(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
|
|
1319
|
+
writeFileSync2(join4(output, "WidgetExtension", "Info.plist"), widgetInfo);
|
|
1177
1320
|
}
|
|
1178
1321
|
if (config.enableWatchApp) {
|
|
1179
|
-
const watchInfo =
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
const watchExtensionInfo =
|
|
1322
|
+
const watchInfo = readFileSync2(join4(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
1323
|
+
writeFileSync2(join4(output, "WatchApp", "Info.plist"), watchInfo);
|
|
1324
|
+
writeFileSync2(join4(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
1325
|
+
const watchExtensionInfo = readFileSync2(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.`)}
|
|
1183
1326
|
${plistString("NSHealthUpdateUsageDescription", `${displayName} writes health data only with your permission.`)}` : "");
|
|
1184
|
-
|
|
1185
|
-
|
|
1327
|
+
writeFileSync2(join4(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
|
|
1328
|
+
writeFileSync2(join4(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
1186
1329
|
}
|
|
1187
|
-
|
|
1330
|
+
writeFileSync2(join4(output, "dist", "index.html"), renderOfflinePage(config));
|
|
1188
1331
|
if (usesNativeBundle(config)) {
|
|
1189
1332
|
if (config.nativeBundle)
|
|
1190
1333
|
installNativeBundle(config.nativeBundle, output);
|
|
@@ -1211,11 +1354,11 @@ async function build(options) {
|
|
|
1211
1354
|
const { htmlPath, nativeBundlePath, devServer, output, generateProject = true } = options;
|
|
1212
1355
|
console.log(`
|
|
1213
1356
|
\uD83D\uDCE6 Building Craft iOS project...`);
|
|
1214
|
-
const configPath =
|
|
1215
|
-
if (!
|
|
1357
|
+
const configPath = join4(output, "craft.config.json");
|
|
1358
|
+
if (!existsSync2(configPath)) {
|
|
1216
1359
|
throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
|
|
1217
1360
|
}
|
|
1218
|
-
const config = JSON.parse(
|
|
1361
|
+
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
1219
1362
|
if (config.renderer !== undefined && config.renderer !== "web" && config.renderer !== "native") {
|
|
1220
1363
|
throw new Error(`Unknown iOS renderer: ${config.renderer}. Expected web or native.`);
|
|
1221
1364
|
}
|
|
@@ -1240,7 +1383,7 @@ async function build(options) {
|
|
|
1240
1383
|
config.devServerURL = devServer;
|
|
1241
1384
|
const origin = new URL(devServer).origin;
|
|
1242
1385
|
config.trustedOrigins = [...new Set([...config.trustedOrigins ?? [], origin])];
|
|
1243
|
-
|
|
1386
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2));
|
|
1244
1387
|
console.log(` Dev server: ${devServer}`);
|
|
1245
1388
|
}
|
|
1246
1389
|
if (htmlPath) {
|
|
@@ -1256,7 +1399,20 @@ async function build(options) {
|
|
|
1256
1399
|
console.warn(" \u26A0 nativeScreens is set but there is no native bundle: every path opens in the web view.");
|
|
1257
1400
|
}
|
|
1258
1401
|
}
|
|
1259
|
-
|
|
1402
|
+
const profilePath = join4(output, "Runtime", "profile.json");
|
|
1403
|
+
const savedProfile = existsSync2(profilePath) ? JSON.parse(readFileSync2(profilePath, "utf8")) : undefined;
|
|
1404
|
+
const runtimeSource = options.runtimeSource === null || options.runtimeDir === null ? undefined : options.runtimeSource ?? process.env.CRAFT_IOS_RUNTIME_SOURCE ?? savedProfile?.source;
|
|
1405
|
+
if (runtimeSource && existsSync2(join4(output, "Runtime"))) {
|
|
1406
|
+
if (options.runtimeDir)
|
|
1407
|
+
throw new Error("Choose runtimeSource or runtimeDir, not both");
|
|
1408
|
+
await installAppRuntime(output, runtimeSource, config);
|
|
1409
|
+
} else {
|
|
1410
|
+
if (savedProfile && savedProfile.mask !== iosPermissionMask(config))
|
|
1411
|
+
throw new Error("Installed Zig runtime permission profile is stale; provide runtimeSource to rebuild it");
|
|
1412
|
+
await refreshRuntime(output, config, options.runtimeDir);
|
|
1413
|
+
}
|
|
1414
|
+
writeShell(output, config);
|
|
1415
|
+
refreshUsageDescriptions(output, config);
|
|
1260
1416
|
if (!generateProject)
|
|
1261
1417
|
return;
|
|
1262
1418
|
try {
|
|
@@ -1283,7 +1439,7 @@ async function open(options) {
|
|
|
1283
1439
|
if (!xcodeproj) {
|
|
1284
1440
|
throw new Error(`No Xcode project found in ${output}. Run 'craft ios build' first.`);
|
|
1285
1441
|
}
|
|
1286
|
-
const projectPath =
|
|
1442
|
+
const projectPath = join4(output, xcodeproj);
|
|
1287
1443
|
console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
|
|
1288
1444
|
await $`open ${projectPath}`;
|
|
1289
1445
|
}
|
|
@@ -1337,8 +1493,8 @@ async function showSimulator() {
|
|
|
1337
1493
|
const selected = await $`xcode-select -p`.quiet().nothrow();
|
|
1338
1494
|
const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
|
|
1339
1495
|
if (developer) {
|
|
1340
|
-
const app =
|
|
1341
|
-
if (
|
|
1496
|
+
const app = join4(developer, "Applications", "Simulator.app");
|
|
1497
|
+
if (existsSync2(app)) {
|
|
1342
1498
|
await $`open ${app}`.quiet().nothrow();
|
|
1343
1499
|
return;
|
|
1344
1500
|
}
|
|
@@ -1353,12 +1509,12 @@ async function run(options) {
|
|
|
1353
1509
|
if (!xcodeproj) {
|
|
1354
1510
|
throw new Error(`No Xcode project found in ${output}`);
|
|
1355
1511
|
}
|
|
1356
|
-
const projectPath =
|
|
1512
|
+
const projectPath = join4(output, xcodeproj);
|
|
1357
1513
|
const appName = xcodeproj.replace(".xcodeproj", "");
|
|
1358
|
-
const configPath =
|
|
1359
|
-
if (!
|
|
1514
|
+
const configPath = join4(output, "craft.config.json");
|
|
1515
|
+
if (!existsSync2(configPath))
|
|
1360
1516
|
throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
|
|
1361
|
-
const config = JSON.parse(
|
|
1517
|
+
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
1362
1518
|
if (simulator) {
|
|
1363
1519
|
console.log("\uD83D\uDCF1 Building and running on simulator...");
|
|
1364
1520
|
const device = await pickSimulator();
|
|
@@ -1368,10 +1524,10 @@ async function run(options) {
|
|
|
1368
1524
|
console.log(` Device: ${device.name} (${device.runtime})`);
|
|
1369
1525
|
try {
|
|
1370
1526
|
await bootSimulator(device);
|
|
1371
|
-
const derivedData =
|
|
1372
|
-
const product =
|
|
1527
|
+
const derivedData = join4(output, "build");
|
|
1528
|
+
const product = join4(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
|
|
1373
1529
|
await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
|
|
1374
|
-
if (!
|
|
1530
|
+
if (!existsSync2(product)) {
|
|
1375
1531
|
throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
|
|
1376
1532
|
}
|
|
1377
1533
|
await $`xcrun simctl install ${device.udid} ${product}`;
|
|
@@ -1477,7 +1633,7 @@ async function main() {
|
|
|
1477
1633
|
});
|
|
1478
1634
|
break;
|
|
1479
1635
|
case "audit-sources": {
|
|
1480
|
-
const root =
|
|
1636
|
+
const root = resolve3(values["app-root"] || ".");
|
|
1481
1637
|
assertNoApplicationSwiftSources(root);
|
|
1482
1638
|
console.log(`\u2705 No application-owned Swift sources found in ${root}`);
|
|
1483
1639
|
break;
|