craft-native 0.0.154 → 0.0.156
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/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +46 -8
- package/dist/ios/src/index.js +289 -98
- package/dist/ios/src/testing.d.ts +71 -0
- package/dist/ios/templates/CraftApp.swift +114 -18
- package/dist/ios/templates/CraftHybrid.swift +269 -31
- package/dist/ios/templates/CraftNativeActions.swift +57 -3
- package/dist/ios/templates/CraftNativeScreen.swift +90 -21
- package/dist/ios/templates/testing/DriverTests.swift.template +305 -0
- package/dist/ios/templates/testing/HostApp.swift +6 -0
- package/dist/ios/templates/testing/MutationValidator.swift +15 -0
- package/dist/ios/templates/testing/project.yml.template +36 -0
- package/package.json +1 -1
package/dist/ios/src/index.js
CHANGED
|
@@ -1,12 +1,149 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../ios/src/index.ts
|
|
3
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
|
|
4
|
-
import { dirname, join, resolve } from "path";
|
|
3
|
+
import { cpSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
4
|
+
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
5
5
|
var {$ } = globalThis.Bun;
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
// ../ios/src/testing.ts
|
|
8
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
9
|
+
import { tmpdir } from "os";
|
|
10
|
+
import { dirname, join } from "path";
|
|
11
|
+
import { spawnSync } from "child_process";
|
|
12
|
+
var templates = join(dirname(import.meta.dir), "templates/testing");
|
|
13
|
+
function bundleIdentifier(value) {
|
|
14
|
+
if (!/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(value))
|
|
15
|
+
throw new Error(`Invalid bundle identifier: ${value}`);
|
|
16
|
+
}
|
|
17
|
+
function swiftString(value) {
|
|
18
|
+
return JSON.stringify(value).replace(/\\u([a-f\d]{4})/gi, "\\u{$1}");
|
|
19
|
+
}
|
|
20
|
+
function initSimulatorDriver(options) {
|
|
21
|
+
const { output, bundleId } = options;
|
|
22
|
+
const name = options.projectName ?? "CraftDriver";
|
|
23
|
+
const prefix = options.bundleIdPrefix ?? `${bundleId}.driver`;
|
|
24
|
+
const version = options.deploymentTarget ?? "17.0";
|
|
25
|
+
const control = options.controlUrl ?? "http://127.0.0.1:3401";
|
|
26
|
+
bundleIdentifier(bundleId);
|
|
27
|
+
bundleIdentifier(prefix);
|
|
28
|
+
if (!/^[A-Za-z][A-Za-z0-9]*$/.test(name))
|
|
29
|
+
throw new Error("projectName must be an alphanumeric Xcode name starting with a letter");
|
|
30
|
+
if (!/^\d+\.\d+$/.test(version) || Number(version) < 17)
|
|
31
|
+
throw new Error("The simulator driver requires iOS 17.0 or newer");
|
|
32
|
+
if (!["http:", "https:"].includes(new URL(control).protocol))
|
|
33
|
+
throw new Error("controlUrl must use HTTP or HTTPS");
|
|
34
|
+
const replacements = {
|
|
35
|
+
PROJECT_NAME: name,
|
|
36
|
+
BUNDLE_PREFIX: prefix,
|
|
37
|
+
IOS_VERSION: version,
|
|
38
|
+
BUNDLE_ID: swiftString(bundleId),
|
|
39
|
+
CONTROL_URL: swiftString(control),
|
|
40
|
+
TABS: `[${(options.tabs ?? []).map(swiftString).join(", ")}]`
|
|
41
|
+
};
|
|
42
|
+
for (const [source, destination] of [
|
|
43
|
+
["HostApp.swift", "Host/HostApp.swift"],
|
|
44
|
+
["DriverTests.swift.template", "UITests/DriverTests.swift"],
|
|
45
|
+
["project.yml.template", "project.yml"]
|
|
46
|
+
]) {
|
|
47
|
+
const path = join(output, destination);
|
|
48
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
49
|
+
const content = readFileSync(join(templates, source), "utf8").replace(/\{\{([A-Z_]+)\}\}/g, (_, key) => replacements[key]);
|
|
50
|
+
let previous;
|
|
51
|
+
try {
|
|
52
|
+
previous = readFileSync(path, "utf8");
|
|
53
|
+
} catch {}
|
|
54
|
+
if (previous !== content)
|
|
55
|
+
writeFileSync(path, content);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function serializeSimulatorSteps(steps) {
|
|
59
|
+
const number = (value) => {
|
|
60
|
+
if (!Number.isFinite(value) || value < 0)
|
|
61
|
+
throw new Error("Simulator values must be finite and nonnegative");
|
|
62
|
+
return value;
|
|
63
|
+
};
|
|
64
|
+
const token = (value) => {
|
|
65
|
+
if (!value || /\s/.test(value))
|
|
66
|
+
throw new Error("Simulator identifiers must be nonempty single tokens");
|
|
67
|
+
return value;
|
|
68
|
+
};
|
|
69
|
+
return steps.map((step) => {
|
|
70
|
+
let arg = "";
|
|
71
|
+
if ("seconds" in step && !("text" in step))
|
|
72
|
+
arg = String(number(step.seconds));
|
|
73
|
+
else if ("milliseconds" in step)
|
|
74
|
+
arg = String(number(step.milliseconds));
|
|
75
|
+
else if ("state" in step)
|
|
76
|
+
arg = step.state;
|
|
77
|
+
else if ("direction" in step)
|
|
78
|
+
arg = step.direction;
|
|
79
|
+
else if ("x" in step) {
|
|
80
|
+
if (number(step.x) > 1 || number(step.y) > 1)
|
|
81
|
+
throw new Error("tapxy coordinates must be between 0 and 1");
|
|
82
|
+
arg = `${step.x} ${step.y}`;
|
|
83
|
+
} else if ("index" in step) {
|
|
84
|
+
if (!Number.isInteger(number(step.index)))
|
|
85
|
+
throw new Error("field index must be an integer");
|
|
86
|
+
arg = `${step.index} ${step.text}`;
|
|
87
|
+
} else if ("id" in step)
|
|
88
|
+
arg = `${token(step.id)} ${step.text}`;
|
|
89
|
+
else if ("text" in step) {
|
|
90
|
+
arg = step.text;
|
|
91
|
+
if ("seconds" in step && step.seconds !== undefined) {
|
|
92
|
+
if (arg.includes("|"))
|
|
93
|
+
throw new Error("A timed expectation cannot contain |");
|
|
94
|
+
arg += ` | ${number(step.seconds)}`;
|
|
95
|
+
}
|
|
96
|
+
} else if ("name" in step)
|
|
97
|
+
arg = token(step.name);
|
|
98
|
+
else if ("url" in step)
|
|
99
|
+
arg = step.url;
|
|
100
|
+
if (/[\r\n]/.test(arg))
|
|
101
|
+
throw new Error("Simulator arguments cannot contain newlines");
|
|
102
|
+
return `${step.command}${arg ? ` ${arg}` : ""}`;
|
|
103
|
+
}).join(`
|
|
104
|
+
`) + `
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
function createNativeMutationValidator() {
|
|
108
|
+
const output = mkdtempSync(join(tmpdir(), "craft-native-mutation-"));
|
|
109
|
+
const binary = join(output, "validate");
|
|
110
|
+
try {
|
|
111
|
+
writeFileSync(join(output, "main.swift"), readFileSync(join(templates, "MutationValidator.swift")));
|
|
112
|
+
const build = Bun.spawnSync(["swiftc", join(templates, "../CraftNativeMutation.swift"), join(output, "main.swift"), "-o", binary]);
|
|
113
|
+
if (build.exitCode !== 0)
|
|
114
|
+
throw new Error(build.stderr.toString());
|
|
115
|
+
} catch (error) {
|
|
116
|
+
rmSync(output, { recursive: true, force: true });
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
let disposed = false;
|
|
120
|
+
return {
|
|
121
|
+
validate(messages) {
|
|
122
|
+
if (disposed)
|
|
123
|
+
throw new Error("The native mutation validator has been disposed");
|
|
124
|
+
const input = join(output, "messages.json");
|
|
125
|
+
writeFileSync(input, JSON.stringify(messages));
|
|
126
|
+
const report = join(output, "result.txt");
|
|
127
|
+
rmSync(report, { force: true });
|
|
128
|
+
const result = spawnSync(binary, [input, report], { encoding: "utf8" });
|
|
129
|
+
let stdout = result.stdout ?? "";
|
|
130
|
+
try {
|
|
131
|
+
stdout = readFileSync(report, "utf8");
|
|
132
|
+
} catch {}
|
|
133
|
+
return { exitCode: result.status ?? 1, stdout, stderr: result.stderr || result.error?.message || (result.signal ? `Validator terminated by ${result.signal}` : "") };
|
|
134
|
+
},
|
|
135
|
+
dispose() {
|
|
136
|
+
disposed = true;
|
|
137
|
+
rmSync(output, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ../ios/src/index.ts
|
|
143
|
+
var TEMPLATES_DIR = join2(dirname2(import.meta.dir), "templates");
|
|
7
144
|
var CRAFT_VERSION = (() => {
|
|
8
145
|
try {
|
|
9
|
-
return JSON.parse(
|
|
146
|
+
return JSON.parse(readFileSync2(join2(dirname2(import.meta.dir), "package.json"), "utf8")).version ?? "0.0.0";
|
|
10
147
|
} catch {
|
|
11
148
|
return "0.0.0";
|
|
12
149
|
}
|
|
@@ -91,11 +228,25 @@ function matchNativePath(nativeScreens, raw) {
|
|
|
91
228
|
function isHybrid(config) {
|
|
92
229
|
return (config.renderer ?? "web") === "web" && Object.keys(config.nativeScreens ?? {}).length > 0;
|
|
93
230
|
}
|
|
231
|
+
function hasNativeRoutes(config) {
|
|
232
|
+
return config.renderer === "native" && Object.keys(config.nativeScreens ?? {}).length > 0;
|
|
233
|
+
}
|
|
234
|
+
function usesNativeBundle(config) {
|
|
235
|
+
return config.renderer === "native" || Object.keys(config.nativeScreens ?? {}).length > 0;
|
|
236
|
+
}
|
|
237
|
+
function nativeStartPath(config) {
|
|
238
|
+
if (!config.devServerURL)
|
|
239
|
+
return "/";
|
|
240
|
+
try {
|
|
241
|
+
const url = new URL(config.devServerURL);
|
|
242
|
+
return `${url.pathname || "/"}${url.search}`;
|
|
243
|
+
} catch {
|
|
244
|
+
return "/";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
94
247
|
function hybridConfigProblems(config) {
|
|
95
248
|
const problems = [];
|
|
96
249
|
const screens = Object.entries(config.nativeScreens ?? {});
|
|
97
|
-
if (screens.length && config.renderer === "native")
|
|
98
|
-
problems.push('nativeScreens mixes native screens into a web app; it needs renderer: "web" (renderer: "native" is all native already).');
|
|
99
250
|
for (const [pattern, screen] of screens) {
|
|
100
251
|
if (normalizeNativePath(pattern) !== pattern || /[?#]/.test(pattern))
|
|
101
252
|
problems.push(`nativeScreens path "${pattern}" must be an absolute path with no query, hash or trailing slash.`);
|
|
@@ -117,13 +268,28 @@ function hybridConfigProblems(config) {
|
|
|
117
268
|
if (!key || typeof target !== "string" || !target)
|
|
118
269
|
problems.push(`shareStorage "${key}" needs the Keychain key native screens read it under.`);
|
|
119
270
|
}
|
|
271
|
+
if (hasNativeRoutes(config)) {
|
|
272
|
+
for (const id of ids) {
|
|
273
|
+
if (!matchNativePath(config.nativeScreens, id))
|
|
274
|
+
problems.push(`Tab "${id}" is not a nativeScreens path; with renderer: "native" every tab must be a native screen.`);
|
|
275
|
+
}
|
|
276
|
+
const start = nativeStartPath(config);
|
|
277
|
+
if (!matchNativePath(config.nativeScreens, start))
|
|
278
|
+
problems.push(`The start path "${start}" (from devServerURL) is not a nativeScreens path; with renderer: "native" the app must start on a native screen.`);
|
|
279
|
+
}
|
|
280
|
+
if (config.nativeApp !== undefined) {
|
|
281
|
+
if (typeof config.nativeApp !== "string" || !/^[A-Za-z_$][\w$-]*$/.test(config.nativeApp))
|
|
282
|
+
problems.push(`nativeApp "${String(config.nativeApp)}" must be a route name from the native bundle.`);
|
|
283
|
+
else if (!usesNativeBundle(config))
|
|
284
|
+
problems.push('nativeApp runs in the native bundle; it needs nativeScreens or renderer: "native".');
|
|
285
|
+
}
|
|
120
286
|
return problems;
|
|
121
287
|
}
|
|
122
288
|
function installNativeBundle(source, output) {
|
|
123
289
|
if (!existsSync(source))
|
|
124
290
|
throw new Error(`Native bundle not found: ${source}`);
|
|
125
|
-
const destination =
|
|
126
|
-
|
|
291
|
+
const destination = join2(output, "dist", "native-screen.js");
|
|
292
|
+
mkdirSync2(dirname2(destination), { recursive: true });
|
|
127
293
|
if (resolve(source) !== resolve(destination))
|
|
128
294
|
cpSync(source, destination);
|
|
129
295
|
}
|
|
@@ -471,7 +637,7 @@ function pngHasAlpha(bytes) {
|
|
|
471
637
|
return false;
|
|
472
638
|
}
|
|
473
639
|
function writeOpaqueIcon(source, target) {
|
|
474
|
-
const bytes = new Uint8Array(
|
|
640
|
+
const bytes = new Uint8Array(readFileSync2(source));
|
|
475
641
|
if (!pngHasAlpha(bytes) || process.platform !== "darwin") {
|
|
476
642
|
cpSync(source, target);
|
|
477
643
|
return;
|
|
@@ -480,27 +646,27 @@ function writeOpaqueIcon(source, target) {
|
|
|
480
646
|
const run = (args) => Bun.spawnSync(["sips", ...args], { stdout: "ignore", stderr: "pipe" });
|
|
481
647
|
const toJpeg = run(["-s", "format", "jpeg", "-s", "formatOptions", "100", source, "--out", flattened]);
|
|
482
648
|
const toPng = toJpeg.exitCode === 0 ? run(["-s", "format", "png", flattened, "--out", target]) : toJpeg;
|
|
483
|
-
|
|
649
|
+
rmSync2(flattened, { force: true });
|
|
484
650
|
if (toPng.exitCode !== 0) {
|
|
485
651
|
cpSync(source, target);
|
|
486
652
|
console.warn(`[craft] Could not remove the app icon's alpha channel; App Store Connect will refuse it: ${toPng.stderr.toString().trim()}`);
|
|
487
653
|
}
|
|
488
654
|
}
|
|
489
655
|
function renderAssetCatalog(output, config) {
|
|
490
|
-
const catalog =
|
|
491
|
-
const appIcon =
|
|
492
|
-
const launchBackground =
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
656
|
+
const catalog = join2(output, "Assets.xcassets");
|
|
657
|
+
const appIcon = join2(catalog, "AppIcon.appiconset");
|
|
658
|
+
const launchBackground = join2(catalog, "LaunchBackground.colorset");
|
|
659
|
+
mkdirSync2(appIcon, { recursive: true });
|
|
660
|
+
mkdirSync2(launchBackground, { recursive: true });
|
|
661
|
+
writeFileSync2(join2(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
|
|
496
662
|
`);
|
|
497
663
|
const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
|
|
498
664
|
if (config.appIconPath) {
|
|
499
665
|
if (!existsSync(config.appIconPath))
|
|
500
666
|
throw new Error(`App icon not found: ${config.appIconPath}`);
|
|
501
|
-
writeOpaqueIcon(config.appIconPath,
|
|
667
|
+
writeOpaqueIcon(config.appIconPath, join2(appIcon, iconFilename));
|
|
502
668
|
}
|
|
503
|
-
|
|
669
|
+
writeFileSync2(join2(appIcon, "Contents.json"), `${JSON.stringify({
|
|
504
670
|
images: iconFilename ? [{
|
|
505
671
|
filename: iconFilename,
|
|
506
672
|
idiom: "universal",
|
|
@@ -527,7 +693,7 @@ function renderAssetCatalog(output, config) {
|
|
|
527
693
|
idiom: "universal"
|
|
528
694
|
});
|
|
529
695
|
}
|
|
530
|
-
|
|
696
|
+
writeFileSync2(join2(launchBackground, "Contents.json"), `${JSON.stringify({
|
|
531
697
|
colors,
|
|
532
698
|
info: { author: "xcode", version: 1 }
|
|
533
699
|
}, null, 2)}
|
|
@@ -536,11 +702,11 @@ function renderAssetCatalog(output, config) {
|
|
|
536
702
|
}
|
|
537
703
|
var LAUNCH_LOGO = "LaunchLogo";
|
|
538
704
|
function renderLaunchLogo(catalog, config) {
|
|
539
|
-
const imageset =
|
|
540
|
-
|
|
705
|
+
const imageset = join2(catalog, `${LAUNCH_LOGO}.imageset`);
|
|
706
|
+
rmSync2(imageset, { recursive: true, force: true });
|
|
541
707
|
if (!config.splashImagePath)
|
|
542
708
|
return;
|
|
543
|
-
|
|
709
|
+
mkdirSync2(imageset, { recursive: true });
|
|
544
710
|
const images = [];
|
|
545
711
|
let vector = false;
|
|
546
712
|
for (const [source, dark] of [[config.splashImagePath, false], [config.splashImagePathDark, true]]) {
|
|
@@ -552,14 +718,14 @@ function renderLaunchLogo(catalog, config) {
|
|
|
552
718
|
if (extension === "svg" || extension === "pdf")
|
|
553
719
|
vector = true;
|
|
554
720
|
const filename = `${LAUNCH_LOGO}${dark ? "-dark" : ""}.${extension}`;
|
|
555
|
-
cpSync(source,
|
|
721
|
+
cpSync(source, join2(imageset, filename));
|
|
556
722
|
images.push({
|
|
557
723
|
...dark ? { appearances: [{ appearance: "luminosity", value: "dark" }] } : {},
|
|
558
724
|
filename,
|
|
559
725
|
idiom: "universal"
|
|
560
726
|
});
|
|
561
727
|
}
|
|
562
|
-
|
|
728
|
+
writeFileSync2(join2(imageset, "Contents.json"), `${JSON.stringify({
|
|
563
729
|
images,
|
|
564
730
|
info: { author: "xcode", version: 1 },
|
|
565
731
|
...vector ? { properties: { "preserves-vector-representation": true } } : {}
|
|
@@ -673,15 +839,15 @@ function syncWebAssets(source, output) {
|
|
|
673
839
|
const sourcePath = resolve(source);
|
|
674
840
|
if (!existsSync(sourcePath))
|
|
675
841
|
throw new Error(`Web asset path not found: ${source}`);
|
|
676
|
-
const distDir =
|
|
677
|
-
|
|
678
|
-
|
|
842
|
+
const distDir = join2(output, "dist");
|
|
843
|
+
rmSync2(distDir, { recursive: true, force: true });
|
|
844
|
+
mkdirSync2(distDir, { recursive: true });
|
|
679
845
|
if (statSync(sourcePath).isDirectory()) {
|
|
680
846
|
cpSync(sourcePath, distDir, { recursive: true });
|
|
681
847
|
} else {
|
|
682
|
-
cpSync(sourcePath,
|
|
848
|
+
cpSync(sourcePath, join2(distDir, "index.html"));
|
|
683
849
|
}
|
|
684
|
-
if (!existsSync(
|
|
850
|
+
if (!existsSync(join2(distDir, "index.html"))) {
|
|
685
851
|
throw new Error(`Web asset directory must contain index.html: ${source}`);
|
|
686
852
|
}
|
|
687
853
|
}
|
|
@@ -703,32 +869,32 @@ var RUNTIME_ARCHIVES = {
|
|
|
703
869
|
};
|
|
704
870
|
async function installRuntime(output, runtimeDir) {
|
|
705
871
|
const resolved = Object.entries(RUNTIME_ARCHIVES).map(([sdk, archives]) => {
|
|
706
|
-
const present = archives.filter((a) => existsSync(
|
|
872
|
+
const present = archives.filter((a) => existsSync(join2(runtimeDir, a)));
|
|
707
873
|
if (present.length === 0) {
|
|
708
874
|
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.`);
|
|
709
875
|
}
|
|
710
876
|
return { sdk, archives, present };
|
|
711
877
|
});
|
|
712
|
-
const dest =
|
|
713
|
-
|
|
878
|
+
const dest = join2(output, "Runtime");
|
|
879
|
+
rmSync2(dest, { recursive: true, force: true });
|
|
714
880
|
for (const { sdk, archives, present } of resolved) {
|
|
715
|
-
const sdkDir =
|
|
716
|
-
|
|
717
|
-
const target =
|
|
881
|
+
const sdkDir = join2(dest, sdk);
|
|
882
|
+
mkdirSync2(sdkDir, { recursive: true });
|
|
883
|
+
const target = join2(sdkDir, "libcraft-ios.a");
|
|
718
884
|
if (present.length === 1) {
|
|
719
885
|
const missing = archives.filter((a) => !present.includes(a));
|
|
720
886
|
if (missing.length > 0) {
|
|
721
887
|
console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
|
|
722
888
|
}
|
|
723
|
-
cpSync(
|
|
889
|
+
cpSync(join2(runtimeDir, present[0]), target);
|
|
724
890
|
} else {
|
|
725
|
-
await $`lipo -create ${present.map((a) =>
|
|
891
|
+
await $`lipo -create ${present.map((a) => join2(runtimeDir, a))} -output ${target}`.quiet();
|
|
726
892
|
}
|
|
727
893
|
}
|
|
728
894
|
return true;
|
|
729
895
|
}
|
|
730
896
|
async function refreshRuntime(output, override) {
|
|
731
|
-
if (!existsSync(
|
|
897
|
+
if (!existsSync(join2(output, "Runtime")))
|
|
732
898
|
return;
|
|
733
899
|
const dir = resolveRuntimeDir(override);
|
|
734
900
|
if (!dir) {
|
|
@@ -745,6 +911,43 @@ function productName(name) {
|
|
|
745
911
|
return "Craft";
|
|
746
912
|
return /^\d/.test(joined) ? `App${joined}` : joined;
|
|
747
913
|
}
|
|
914
|
+
function restoreShell(output) {
|
|
915
|
+
const config = JSON.parse(readFileSync2(join2(output, "craft.config.json"), "utf8"));
|
|
916
|
+
writeShell(output, config);
|
|
917
|
+
}
|
|
918
|
+
function writeShell(output, config) {
|
|
919
|
+
if (!config.appName || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(config.bundleId))
|
|
920
|
+
throw new Error("The iOS shell needs an appName and a valid bundleId in craft.config.json.");
|
|
921
|
+
const name = productName(config.appName);
|
|
922
|
+
mkdirSync2(join2(output, "Sources"), { recursive: true });
|
|
923
|
+
mkdirSync2(join2(output, "Shared"), { recursive: true });
|
|
924
|
+
const swiftTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
|
|
925
|
+
const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, config.bundleId).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION);
|
|
926
|
+
writeFileSync2(join2(output, "Sources", `${name}App.swift`), swiftSource);
|
|
927
|
+
cpSync(join2(TEMPLATES_DIR, "CraftNativeScreen.swift"), join2(output, "Sources", "CraftNativeScreen.swift"));
|
|
928
|
+
cpSync(join2(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join2(output, "Sources", "CraftNativeFlatList.swift"));
|
|
929
|
+
cpSync(join2(TEMPLATES_DIR, "CraftNativeActions.swift"), join2(output, "Sources", "CraftNativeActions.swift"));
|
|
930
|
+
cpSync(join2(TEMPLATES_DIR, "CraftHealth.swift"), join2(output, "Sources", "CraftHealth.swift"));
|
|
931
|
+
cpSync(join2(TEMPLATES_DIR, "CraftMusic.swift"), join2(output, "Sources", "CraftMusic.swift"));
|
|
932
|
+
cpSync(join2(TEMPLATES_DIR, "CraftHeartRate.swift"), join2(output, "Sources", "CraftHeartRate.swift"));
|
|
933
|
+
cpSync(join2(TEMPLATES_DIR, "CraftNativeMutation.swift"), join2(output, "Sources", "CraftNativeMutation.swift"));
|
|
934
|
+
cpSync(join2(TEMPLATES_DIR, "CraftNativeUI.swift"), join2(output, "Sources", "CraftNativeUI.swift"));
|
|
935
|
+
cpSync(join2(TEMPLATES_DIR, "CraftNativeServices.swift"), join2(output, "Sources", "CraftNativeServices.swift"));
|
|
936
|
+
cpSync(join2(TEMPLATES_DIR, "CraftNativeFiles.swift"), join2(output, "Sources", "CraftNativeFiles.swift"));
|
|
937
|
+
cpSync(join2(TEMPLATES_DIR, "CraftWebAudio.swift"), join2(output, "Sources", "CraftWebAudio.swift"));
|
|
938
|
+
cpSync(join2(TEMPLATES_DIR, "CraftHybrid.swift"), join2(output, "Sources", "CraftHybrid.swift"));
|
|
939
|
+
cpSync(join2(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join2(output, "Shared", "CraftActivityAttributes.swift"));
|
|
940
|
+
for (const [enabled, folder, template, filename] of [
|
|
941
|
+
[config.enableLiveActivities, "WidgetExtension", "CraftLiveActivityWidget.swift.template", `${name}LiveActivity.swift`],
|
|
942
|
+
[config.enableWatchApp, "WatchExtension", "CraftWatchApp.swift.template", `${name}WatchApp.swift`]
|
|
943
|
+
]) {
|
|
944
|
+
if (!enabled)
|
|
945
|
+
continue;
|
|
946
|
+
mkdirSync2(join2(output, folder), { recursive: true });
|
|
947
|
+
const source = readFileSync2(join2(TEMPLATES_DIR, template), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
|
|
948
|
+
writeFileSync2(join2(output, folder, filename), source);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
748
951
|
async function init(options) {
|
|
749
952
|
const { name: displayName, bundleId, teamId, output } = options;
|
|
750
953
|
const name = productName(displayName);
|
|
@@ -752,12 +955,12 @@ async function init(options) {
|
|
|
752
955
|
\u26A1 Initializing Craft iOS project: ${displayName}`);
|
|
753
956
|
console.log(` Output: ${output}
|
|
754
957
|
`);
|
|
755
|
-
const dirs = [output,
|
|
958
|
+
const dirs = [output, join2(output, "Sources"), join2(output, "Shared"), join2(output, "dist")];
|
|
756
959
|
if (options.config?.enableWatchApp)
|
|
757
|
-
dirs.push(
|
|
960
|
+
dirs.push(join2(output, "WatchApp"), join2(output, "WatchExtension"));
|
|
758
961
|
for (const dir of dirs) {
|
|
759
962
|
if (!existsSync(dir)) {
|
|
760
|
-
|
|
963
|
+
mkdirSync2(dir, { recursive: true });
|
|
761
964
|
}
|
|
762
965
|
}
|
|
763
966
|
const finalBundleId = bundleId || `com.craft.${name.toLowerCase()}`;
|
|
@@ -781,29 +984,15 @@ async function init(options) {
|
|
|
781
984
|
`));
|
|
782
985
|
if (Object.keys(config.shareStorage ?? {}).length)
|
|
783
986
|
config.enableSecureStorage = true;
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
const
|
|
787
|
-
writeFileSync(join(output, "Sources", `${name}App.swift`), swiftSource);
|
|
788
|
-
cpSync(join(TEMPLATES_DIR, "CraftNativeScreen.swift"), join(output, "Sources", "CraftNativeScreen.swift"));
|
|
789
|
-
cpSync(join(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join(output, "Sources", "CraftNativeFlatList.swift"));
|
|
790
|
-
cpSync(join(TEMPLATES_DIR, "CraftNativeActions.swift"), join(output, "Sources", "CraftNativeActions.swift"));
|
|
791
|
-
cpSync(join(TEMPLATES_DIR, "CraftHealth.swift"), join(output, "Sources", "CraftHealth.swift"));
|
|
792
|
-
cpSync(join(TEMPLATES_DIR, "CraftMusic.swift"), join(output, "Sources", "CraftMusic.swift"));
|
|
793
|
-
cpSync(join(TEMPLATES_DIR, "CraftHeartRate.swift"), join(output, "Sources", "CraftHeartRate.swift"));
|
|
794
|
-
cpSync(join(TEMPLATES_DIR, "CraftNativeMutation.swift"), join(output, "Sources", "CraftNativeMutation.swift"));
|
|
795
|
-
cpSync(join(TEMPLATES_DIR, "CraftNativeUI.swift"), join(output, "Sources", "CraftNativeUI.swift"));
|
|
796
|
-
cpSync(join(TEMPLATES_DIR, "CraftNativeServices.swift"), join(output, "Sources", "CraftNativeServices.swift"));
|
|
797
|
-
cpSync(join(TEMPLATES_DIR, "CraftNativeFiles.swift"), join(output, "Sources", "CraftNativeFiles.swift"));
|
|
798
|
-
cpSync(join(TEMPLATES_DIR, "CraftWebAudio.swift"), join(output, "Sources", "CraftWebAudio.swift"));
|
|
799
|
-
cpSync(join(TEMPLATES_DIR, "CraftHybrid.swift"), join(output, "Sources", "CraftHybrid.swift"));
|
|
800
|
-
const infoPlistTemplate = readFileSync(join(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
|
|
987
|
+
writeFileSync2(join2(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
988
|
+
writeShell(output, config);
|
|
989
|
+
const infoPlistTemplate = readFileSync2(join2(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
|
|
801
990
|
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(/\{\{NON_EXEMPT_ENCRYPTION\}\}/g, config.usesNonExemptEncryption ? "true" : "false").replace(/\{\{LIVE_ACTIVITY_SUPPORT\}\}/g, config.enableLiveActivities ? ` <key>NSSupportsLiveActivities</key>
|
|
802
991
|
<true/>
|
|
803
992
|
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
|
|
804
993
|
<true/>` : "");
|
|
805
|
-
|
|
806
|
-
const projectYmlTemplate =
|
|
994
|
+
writeFileSync2(join2(output, "Info.plist"), infoPlist);
|
|
995
|
+
const projectYmlTemplate = readFileSync2(join2(TEMPLATES_DIR, "project.yml.template"), "utf-8");
|
|
807
996
|
const nativeDependencies = [];
|
|
808
997
|
if (config.enableLiveActivities)
|
|
809
998
|
nativeDependencies.push(` - target: ${name}LiveActivity`);
|
|
@@ -860,40 +1049,37 @@ async function init(options) {
|
|
|
860
1049
|
if (hasRuntime) {
|
|
861
1050
|
console.log(" Linked the Zig runtime from", runtimeDir);
|
|
862
1051
|
} else {
|
|
863
|
-
|
|
1052
|
+
rmSync2(join2(output, "Runtime"), { recursive: true, force: true });
|
|
864
1053
|
}
|
|
865
1054
|
const projectYml = projectYmlTemplate.replace(/\{\{CRAFT_RUNTIME_SETTINGS\}\}/g, hasRuntime ? renderRuntimeSettings() : "").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{BUNDLE_ID_PREFIX\}\}/g, bundleIdPrefix).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{IOS_VERSION\}\}/g, config.iosVersion || "15.0").replace(/\{\{DEVICE_FAMILIES\}\}/g, renderDeviceFamilies(config)).replace(/\{\{TEAM_ID\}\}/g, teamId || "").replace(/\{\{NATIVE_DEPENDENCIES\}\}/g, nativeDependencies.length ? ` dependencies:
|
|
866
1055
|
${nativeDependencies.join(`
|
|
867
1056
|
`)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
|
|
868
1057
|
`));
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1058
|
+
writeFileSync2(join2(output, "project.yml"), projectYml);
|
|
1059
|
+
writeFileSync2(join2(output, "Craft.entitlements"), renderEntitlements(config));
|
|
1060
|
+
writeFileSync2(join2(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
|
|
872
1061
|
renderAssetCatalog(output, config);
|
|
873
|
-
cpSync(join(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join(output, "Shared", "CraftActivityAttributes.swift"));
|
|
874
1062
|
if (config.enableLiveActivities) {
|
|
875
|
-
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
const widgetInfo = readFileSync(join(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
|
|
879
|
-
writeFileSync(join(output, "WidgetExtension", "Info.plist"), widgetInfo);
|
|
1063
|
+
mkdirSync2(join2(output, "WidgetExtension"), { recursive: true });
|
|
1064
|
+
const widgetInfo = readFileSync2(join2(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
|
|
1065
|
+
writeFileSync2(join2(output, "WidgetExtension", "Info.plist"), widgetInfo);
|
|
880
1066
|
}
|
|
881
1067
|
if (config.enableWatchApp) {
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
writeFileSync(join(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
|
|
889
|
-
writeFileSync(join(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
1068
|
+
const watchInfo = readFileSync2(join2(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
1069
|
+
writeFileSync2(join2(output, "WatchApp", "Info.plist"), watchInfo);
|
|
1070
|
+
writeFileSync2(join2(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
1071
|
+
const watchExtensionInfo = readFileSync2(join2(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
1072
|
+
writeFileSync2(join2(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
|
|
1073
|
+
writeFileSync2(join2(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
890
1074
|
}
|
|
891
|
-
|
|
892
|
-
if (
|
|
1075
|
+
writeFileSync2(join2(output, "dist", "index.html"), renderOfflinePage(config));
|
|
1076
|
+
if (usesNativeBundle(config)) {
|
|
893
1077
|
if (config.nativeBundle)
|
|
894
1078
|
installNativeBundle(config.nativeBundle, output);
|
|
895
|
-
else
|
|
1079
|
+
else if (isHybrid(config))
|
|
896
1080
|
console.warn(" \u26A0 nativeScreens is set without nativeBundle: every path opens in the web view until `craft ios build --native-bundle` supplies one.");
|
|
1081
|
+
else if (hasNativeRoutes(config))
|
|
1082
|
+
console.warn(" \u26A0 nativeScreens is set without nativeBundle: the app has no screens until `craft ios build --native-bundle` supplies one.");
|
|
897
1083
|
}
|
|
898
1084
|
console.log("\u2705 Project initialized");
|
|
899
1085
|
console.log("");
|
|
@@ -913,11 +1099,11 @@ async function build(options) {
|
|
|
913
1099
|
const { htmlPath, nativeBundlePath, devServer, output, generateProject = true } = options;
|
|
914
1100
|
console.log(`
|
|
915
1101
|
\uD83D\uDCE6 Building Craft iOS project...`);
|
|
916
|
-
const configPath =
|
|
1102
|
+
const configPath = join2(output, "craft.config.json");
|
|
917
1103
|
if (!existsSync(configPath)) {
|
|
918
1104
|
throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
|
|
919
1105
|
}
|
|
920
|
-
const config = JSON.parse(
|
|
1106
|
+
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
921
1107
|
if (config.renderer !== undefined && config.renderer !== "web" && config.renderer !== "native") {
|
|
922
1108
|
throw new Error(`Unknown iOS renderer: ${config.renderer}. Expected web or native.`);
|
|
923
1109
|
}
|
|
@@ -925,12 +1111,10 @@ async function build(options) {
|
|
|
925
1111
|
if (htmlPath || devServer) {
|
|
926
1112
|
throw new Error("A native iOS screen cannot use --html-path or --dev-server.");
|
|
927
1113
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
}
|
|
933
|
-
if (!existsSync(join(output, "dist", "native-screen.js"))) {
|
|
1114
|
+
const bundle = nativeBundlePath ?? config.nativeBundle;
|
|
1115
|
+
if (bundle)
|
|
1116
|
+
installNativeBundle(bundle, output);
|
|
1117
|
+
if (!existsSync(join2(output, "dist", "native-screen.js"))) {
|
|
934
1118
|
throw new Error("Native iOS mode needs dist/native-screen.js. Compile a .stx screen with `stx native compile` first.");
|
|
935
1119
|
}
|
|
936
1120
|
} else if (nativeBundlePath && !isHybrid(config)) {
|
|
@@ -944,7 +1128,7 @@ async function build(options) {
|
|
|
944
1128
|
config.devServerURL = devServer;
|
|
945
1129
|
const origin = new URL(devServer).origin;
|
|
946
1130
|
config.trustedOrigins = [...new Set([...config.trustedOrigins ?? [], origin])];
|
|
947
|
-
|
|
1131
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2));
|
|
948
1132
|
console.log(` Dev server: ${devServer}`);
|
|
949
1133
|
}
|
|
950
1134
|
if (htmlPath) {
|
|
@@ -956,7 +1140,7 @@ async function build(options) {
|
|
|
956
1140
|
if (bundle) {
|
|
957
1141
|
installNativeBundle(bundle, output);
|
|
958
1142
|
console.log(` Native screens: ${bundle} \u2192 dist/native-screen.js`);
|
|
959
|
-
} else if (!existsSync(
|
|
1143
|
+
} else if (!existsSync(join2(output, "dist", "native-screen.js"))) {
|
|
960
1144
|
console.warn(" \u26A0 nativeScreens is set but there is no native bundle: every path opens in the web view.");
|
|
961
1145
|
}
|
|
962
1146
|
}
|
|
@@ -986,7 +1170,7 @@ async function open(options) {
|
|
|
986
1170
|
if (!xcodeproj) {
|
|
987
1171
|
throw new Error(`No Xcode project found in ${output}. Run 'craft ios build' first.`);
|
|
988
1172
|
}
|
|
989
|
-
const projectPath =
|
|
1173
|
+
const projectPath = join2(output, xcodeproj);
|
|
990
1174
|
console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
|
|
991
1175
|
await $`open ${projectPath}`;
|
|
992
1176
|
}
|
|
@@ -1027,7 +1211,7 @@ async function showSimulator() {
|
|
|
1027
1211
|
const selected = await $`xcode-select -p`.quiet().nothrow();
|
|
1028
1212
|
const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
|
|
1029
1213
|
if (developer) {
|
|
1030
|
-
const app =
|
|
1214
|
+
const app = join2(developer, "Applications", "Simulator.app");
|
|
1031
1215
|
if (existsSync(app)) {
|
|
1032
1216
|
await $`open ${app}`.quiet().nothrow();
|
|
1033
1217
|
return;
|
|
@@ -1043,12 +1227,12 @@ async function run(options) {
|
|
|
1043
1227
|
if (!xcodeproj) {
|
|
1044
1228
|
throw new Error(`No Xcode project found in ${output}`);
|
|
1045
1229
|
}
|
|
1046
|
-
const projectPath =
|
|
1230
|
+
const projectPath = join2(output, xcodeproj);
|
|
1047
1231
|
const appName = xcodeproj.replace(".xcodeproj", "");
|
|
1048
|
-
const configPath =
|
|
1232
|
+
const configPath = join2(output, "craft.config.json");
|
|
1049
1233
|
if (!existsSync(configPath))
|
|
1050
1234
|
throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
|
|
1051
|
-
const config = JSON.parse(
|
|
1235
|
+
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
1052
1236
|
if (simulator) {
|
|
1053
1237
|
console.log("\uD83D\uDCF1 Building and running on simulator...");
|
|
1054
1238
|
const device = await pickSimulator();
|
|
@@ -1058,8 +1242,8 @@ async function run(options) {
|
|
|
1058
1242
|
console.log(` Device: ${device.name} (${device.runtime})`);
|
|
1059
1243
|
try {
|
|
1060
1244
|
await bootSimulator(device);
|
|
1061
|
-
const derivedData =
|
|
1062
|
-
const product =
|
|
1245
|
+
const derivedData = join2(output, "build");
|
|
1246
|
+
const product = join2(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
|
|
1063
1247
|
await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
|
|
1064
1248
|
if (!existsSync(product)) {
|
|
1065
1249
|
throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
|
|
@@ -1083,9 +1267,12 @@ async function run(options) {
|
|
|
1083
1267
|
}
|
|
1084
1268
|
}
|
|
1085
1269
|
export {
|
|
1270
|
+
usesNativeBundle,
|
|
1086
1271
|
syncWebAssets,
|
|
1087
1272
|
showSimulator,
|
|
1273
|
+
serializeSimulatorSteps,
|
|
1088
1274
|
run,
|
|
1275
|
+
restoreShell,
|
|
1089
1276
|
resolveRuntimeDir,
|
|
1090
1277
|
resolveAppearance,
|
|
1091
1278
|
renderWatchEntitlements,
|
|
@@ -1109,11 +1296,15 @@ export {
|
|
|
1109
1296
|
orderSimulators,
|
|
1110
1297
|
open,
|
|
1111
1298
|
normalizeNativePath,
|
|
1299
|
+
nativeStartPath,
|
|
1112
1300
|
matchNativePath,
|
|
1113
1301
|
isHybrid,
|
|
1114
1302
|
installRuntime,
|
|
1303
|
+
initSimulatorDriver,
|
|
1115
1304
|
init,
|
|
1116
1305
|
hybridConfigProblems,
|
|
1306
|
+
hasNativeRoutes,
|
|
1307
|
+
createNativeMutationValidator,
|
|
1117
1308
|
build,
|
|
1118
1309
|
bootSimulator,
|
|
1119
1310
|
backgroundRefreshIdentifier,
|