@craft-native/ios 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 +192 -83
- package/dist/index.d.ts +46 -8
- package/dist/index.js +289 -98
- package/dist/testing.d.ts +71 -0
- package/package.json +1 -1
- package/templates/CraftApp.swift +114 -18
- package/templates/CraftHybrid.swift +269 -31
- package/templates/CraftNativeActions.swift +57 -3
- package/templates/CraftNativeScreen.swift +90 -21
- package/templates/testing/DriverTests.swift.template +305 -0
- package/templates/testing/HostApp.swift +6 -0
- package/templates/testing/MutationValidator.swift +15 -0
- package/templates/testing/project.yml.template +36 -0
package/dist/cli.js
CHANGED
|
@@ -6,12 +6,18 @@ import { parseArgs } from "util";
|
|
|
6
6
|
|
|
7
7
|
// src/index.ts
|
|
8
8
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
|
|
9
|
-
import { dirname, join, resolve } from "path";
|
|
9
|
+
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
10
10
|
var {$ } = globalThis.Bun;
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
// src/testing.ts
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
|
+
var templates = join(dirname(import.meta.dir), "templates/testing");
|
|
15
|
+
|
|
16
|
+
// src/index.ts
|
|
17
|
+
var TEMPLATES_DIR = join2(dirname2(import.meta.dir), "templates");
|
|
12
18
|
var CRAFT_VERSION = (() => {
|
|
13
19
|
try {
|
|
14
|
-
return JSON.parse(readFileSync(
|
|
20
|
+
return JSON.parse(readFileSync(join2(dirname2(import.meta.dir), "package.json"), "utf8")).version ?? "0.0.0";
|
|
15
21
|
} catch {
|
|
16
22
|
return "0.0.0";
|
|
17
23
|
}
|
|
@@ -33,14 +39,88 @@ function normalizeNativePath(raw) {
|
|
|
33
39
|
function routeSegments(path) {
|
|
34
40
|
return path === "/" ? [] : path.slice(1).split("/");
|
|
35
41
|
}
|
|
42
|
+
function matchNativePath(nativeScreens, raw) {
|
|
43
|
+
const path = normalizeNativePath(raw);
|
|
44
|
+
if (path === null)
|
|
45
|
+
return null;
|
|
46
|
+
const routes = Object.entries(nativeScreens ?? {}).map(([pattern, screen]) => ({ pattern: normalizeNativePath(pattern), screen })).filter((route) => route.pattern !== null && !!route.screen).map((route) => ({ ...route, segments: routeSegments(route.pattern) }));
|
|
47
|
+
const wild = (segments) => segments.at(-1) === "*" ? 1 : 0;
|
|
48
|
+
const fixed = (segments) => segments.filter((s) => s !== "*" && !s.startsWith(":")).length;
|
|
49
|
+
routes.sort((a, b) => wild(a.segments) - wild(b.segments) || fixed(b.segments) - fixed(a.segments) || b.segments.length - a.segments.length || (a.pattern < b.pattern ? -1 : a.pattern > b.pattern ? 1 : 0));
|
|
50
|
+
const decode = (value) => {
|
|
51
|
+
try {
|
|
52
|
+
return decodeURIComponent(value);
|
|
53
|
+
} catch {
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const parts = routeSegments(path);
|
|
58
|
+
for (const route of routes) {
|
|
59
|
+
const params = {};
|
|
60
|
+
let matched = true;
|
|
61
|
+
for (let i = 0;i < route.segments.length; i++) {
|
|
62
|
+
const segment = route.segments[i];
|
|
63
|
+
if (segment === "*") {
|
|
64
|
+
params["*"] = parts.slice(i).join("/");
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
if (i >= parts.length) {
|
|
68
|
+
matched = false;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
if (segment.startsWith(":")) {
|
|
72
|
+
if (!parts[i]) {
|
|
73
|
+
matched = false;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
params[segment.slice(1)] = decode(parts[i]);
|
|
77
|
+
} else if (segment !== parts[i]) {
|
|
78
|
+
matched = false;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!matched)
|
|
83
|
+
continue;
|
|
84
|
+
if (route.segments.at(-1) !== "*" && route.segments.length !== parts.length)
|
|
85
|
+
continue;
|
|
86
|
+
const query = raw.indexOf("?");
|
|
87
|
+
if (query >= 0) {
|
|
88
|
+
for (const item of raw.slice(query + 1).split("#")[0].split("&")) {
|
|
89
|
+
if (!item)
|
|
90
|
+
continue;
|
|
91
|
+
const [rawKey = "", ...rest] = item.split("=");
|
|
92
|
+
const key = decode(rawKey);
|
|
93
|
+
if (!key || key in params)
|
|
94
|
+
continue;
|
|
95
|
+
params[key] = decode(rest.join("=").replace(/\+/g, " "));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { path, screen: route.screen, params };
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
36
102
|
function isHybrid(config) {
|
|
37
103
|
return (config.renderer ?? "web") === "web" && Object.keys(config.nativeScreens ?? {}).length > 0;
|
|
38
104
|
}
|
|
105
|
+
function hasNativeRoutes(config) {
|
|
106
|
+
return config.renderer === "native" && Object.keys(config.nativeScreens ?? {}).length > 0;
|
|
107
|
+
}
|
|
108
|
+
function usesNativeBundle(config) {
|
|
109
|
+
return config.renderer === "native" || Object.keys(config.nativeScreens ?? {}).length > 0;
|
|
110
|
+
}
|
|
111
|
+
function nativeStartPath(config) {
|
|
112
|
+
if (!config.devServerURL)
|
|
113
|
+
return "/";
|
|
114
|
+
try {
|
|
115
|
+
const url = new URL(config.devServerURL);
|
|
116
|
+
return `${url.pathname || "/"}${url.search}`;
|
|
117
|
+
} catch {
|
|
118
|
+
return "/";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
39
121
|
function hybridConfigProblems(config) {
|
|
40
122
|
const problems = [];
|
|
41
123
|
const screens = Object.entries(config.nativeScreens ?? {});
|
|
42
|
-
if (screens.length && config.renderer === "native")
|
|
43
|
-
problems.push('nativeScreens mixes native screens into a web app; it needs renderer: "web" (renderer: "native" is all native already).');
|
|
44
124
|
for (const [pattern, screen] of screens) {
|
|
45
125
|
if (normalizeNativePath(pattern) !== pattern || /[?#]/.test(pattern))
|
|
46
126
|
problems.push(`nativeScreens path "${pattern}" must be an absolute path with no query, hash or trailing slash.`);
|
|
@@ -62,13 +142,28 @@ function hybridConfigProblems(config) {
|
|
|
62
142
|
if (!key || typeof target !== "string" || !target)
|
|
63
143
|
problems.push(`shareStorage "${key}" needs the Keychain key native screens read it under.`);
|
|
64
144
|
}
|
|
145
|
+
if (hasNativeRoutes(config)) {
|
|
146
|
+
for (const id of ids) {
|
|
147
|
+
if (!matchNativePath(config.nativeScreens, id))
|
|
148
|
+
problems.push(`Tab "${id}" is not a nativeScreens path; with renderer: "native" every tab must be a native screen.`);
|
|
149
|
+
}
|
|
150
|
+
const start = nativeStartPath(config);
|
|
151
|
+
if (!matchNativePath(config.nativeScreens, start))
|
|
152
|
+
problems.push(`The start path "${start}" (from devServerURL) is not a nativeScreens path; with renderer: "native" the app must start on a native screen.`);
|
|
153
|
+
}
|
|
154
|
+
if (config.nativeApp !== undefined) {
|
|
155
|
+
if (typeof config.nativeApp !== "string" || !/^[A-Za-z_$][\w$-]*$/.test(config.nativeApp))
|
|
156
|
+
problems.push(`nativeApp "${String(config.nativeApp)}" must be a route name from the native bundle.`);
|
|
157
|
+
else if (!usesNativeBundle(config))
|
|
158
|
+
problems.push('nativeApp runs in the native bundle; it needs nativeScreens or renderer: "native".');
|
|
159
|
+
}
|
|
65
160
|
return problems;
|
|
66
161
|
}
|
|
67
162
|
function installNativeBundle(source, output) {
|
|
68
163
|
if (!existsSync(source))
|
|
69
164
|
throw new Error(`Native bundle not found: ${source}`);
|
|
70
|
-
const destination =
|
|
71
|
-
mkdirSync(
|
|
165
|
+
const destination = join2(output, "dist", "native-screen.js");
|
|
166
|
+
mkdirSync(dirname2(destination), { recursive: true });
|
|
72
167
|
if (resolve(source) !== resolve(destination))
|
|
73
168
|
cpSync(source, destination);
|
|
74
169
|
}
|
|
@@ -432,20 +527,20 @@ function writeOpaqueIcon(source, target) {
|
|
|
432
527
|
}
|
|
433
528
|
}
|
|
434
529
|
function renderAssetCatalog(output, config) {
|
|
435
|
-
const catalog =
|
|
436
|
-
const appIcon =
|
|
437
|
-
const launchBackground =
|
|
530
|
+
const catalog = join2(output, "Assets.xcassets");
|
|
531
|
+
const appIcon = join2(catalog, "AppIcon.appiconset");
|
|
532
|
+
const launchBackground = join2(catalog, "LaunchBackground.colorset");
|
|
438
533
|
mkdirSync(appIcon, { recursive: true });
|
|
439
534
|
mkdirSync(launchBackground, { recursive: true });
|
|
440
|
-
writeFileSync(
|
|
535
|
+
writeFileSync(join2(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
|
|
441
536
|
`);
|
|
442
537
|
const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
|
|
443
538
|
if (config.appIconPath) {
|
|
444
539
|
if (!existsSync(config.appIconPath))
|
|
445
540
|
throw new Error(`App icon not found: ${config.appIconPath}`);
|
|
446
|
-
writeOpaqueIcon(config.appIconPath,
|
|
541
|
+
writeOpaqueIcon(config.appIconPath, join2(appIcon, iconFilename));
|
|
447
542
|
}
|
|
448
|
-
writeFileSync(
|
|
543
|
+
writeFileSync(join2(appIcon, "Contents.json"), `${JSON.stringify({
|
|
449
544
|
images: iconFilename ? [{
|
|
450
545
|
filename: iconFilename,
|
|
451
546
|
idiom: "universal",
|
|
@@ -472,7 +567,7 @@ function renderAssetCatalog(output, config) {
|
|
|
472
567
|
idiom: "universal"
|
|
473
568
|
});
|
|
474
569
|
}
|
|
475
|
-
writeFileSync(
|
|
570
|
+
writeFileSync(join2(launchBackground, "Contents.json"), `${JSON.stringify({
|
|
476
571
|
colors,
|
|
477
572
|
info: { author: "xcode", version: 1 }
|
|
478
573
|
}, null, 2)}
|
|
@@ -481,7 +576,7 @@ function renderAssetCatalog(output, config) {
|
|
|
481
576
|
}
|
|
482
577
|
var LAUNCH_LOGO = "LaunchLogo";
|
|
483
578
|
function renderLaunchLogo(catalog, config) {
|
|
484
|
-
const imageset =
|
|
579
|
+
const imageset = join2(catalog, `${LAUNCH_LOGO}.imageset`);
|
|
485
580
|
rmSync(imageset, { recursive: true, force: true });
|
|
486
581
|
if (!config.splashImagePath)
|
|
487
582
|
return;
|
|
@@ -497,14 +592,14 @@ function renderLaunchLogo(catalog, config) {
|
|
|
497
592
|
if (extension === "svg" || extension === "pdf")
|
|
498
593
|
vector = true;
|
|
499
594
|
const filename = `${LAUNCH_LOGO}${dark ? "-dark" : ""}.${extension}`;
|
|
500
|
-
cpSync(source,
|
|
595
|
+
cpSync(source, join2(imageset, filename));
|
|
501
596
|
images.push({
|
|
502
597
|
...dark ? { appearances: [{ appearance: "luminosity", value: "dark" }] } : {},
|
|
503
598
|
filename,
|
|
504
599
|
idiom: "universal"
|
|
505
600
|
});
|
|
506
601
|
}
|
|
507
|
-
writeFileSync(
|
|
602
|
+
writeFileSync(join2(imageset, "Contents.json"), `${JSON.stringify({
|
|
508
603
|
images,
|
|
509
604
|
info: { author: "xcode", version: 1 },
|
|
510
605
|
...vector ? { properties: { "preserves-vector-representation": true } } : {}
|
|
@@ -618,15 +713,15 @@ function syncWebAssets(source, output) {
|
|
|
618
713
|
const sourcePath = resolve(source);
|
|
619
714
|
if (!existsSync(sourcePath))
|
|
620
715
|
throw new Error(`Web asset path not found: ${source}`);
|
|
621
|
-
const distDir =
|
|
716
|
+
const distDir = join2(output, "dist");
|
|
622
717
|
rmSync(distDir, { recursive: true, force: true });
|
|
623
718
|
mkdirSync(distDir, { recursive: true });
|
|
624
719
|
if (statSync(sourcePath).isDirectory()) {
|
|
625
720
|
cpSync(sourcePath, distDir, { recursive: true });
|
|
626
721
|
} else {
|
|
627
|
-
cpSync(sourcePath,
|
|
722
|
+
cpSync(sourcePath, join2(distDir, "index.html"));
|
|
628
723
|
}
|
|
629
|
-
if (!existsSync(
|
|
724
|
+
if (!existsSync(join2(distDir, "index.html"))) {
|
|
630
725
|
throw new Error(`Web asset directory must contain index.html: ${source}`);
|
|
631
726
|
}
|
|
632
727
|
}
|
|
@@ -648,32 +743,32 @@ var RUNTIME_ARCHIVES = {
|
|
|
648
743
|
};
|
|
649
744
|
async function installRuntime(output, runtimeDir) {
|
|
650
745
|
const resolved = Object.entries(RUNTIME_ARCHIVES).map(([sdk, archives]) => {
|
|
651
|
-
const present = archives.filter((a) => existsSync(
|
|
746
|
+
const present = archives.filter((a) => existsSync(join2(runtimeDir, a)));
|
|
652
747
|
if (present.length === 0) {
|
|
653
748
|
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.`);
|
|
654
749
|
}
|
|
655
750
|
return { sdk, archives, present };
|
|
656
751
|
});
|
|
657
|
-
const dest =
|
|
752
|
+
const dest = join2(output, "Runtime");
|
|
658
753
|
rmSync(dest, { recursive: true, force: true });
|
|
659
754
|
for (const { sdk, archives, present } of resolved) {
|
|
660
|
-
const sdkDir =
|
|
755
|
+
const sdkDir = join2(dest, sdk);
|
|
661
756
|
mkdirSync(sdkDir, { recursive: true });
|
|
662
|
-
const target =
|
|
757
|
+
const target = join2(sdkDir, "libcraft-ios.a");
|
|
663
758
|
if (present.length === 1) {
|
|
664
759
|
const missing = archives.filter((a) => !present.includes(a));
|
|
665
760
|
if (missing.length > 0) {
|
|
666
761
|
console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
|
|
667
762
|
}
|
|
668
|
-
cpSync(
|
|
763
|
+
cpSync(join2(runtimeDir, present[0]), target);
|
|
669
764
|
} else {
|
|
670
|
-
await $`lipo -create ${present.map((a) =>
|
|
765
|
+
await $`lipo -create ${present.map((a) => join2(runtimeDir, a))} -output ${target}`.quiet();
|
|
671
766
|
}
|
|
672
767
|
}
|
|
673
768
|
return true;
|
|
674
769
|
}
|
|
675
770
|
async function refreshRuntime(output, override) {
|
|
676
|
-
if (!existsSync(
|
|
771
|
+
if (!existsSync(join2(output, "Runtime")))
|
|
677
772
|
return;
|
|
678
773
|
const dir = resolveRuntimeDir(override);
|
|
679
774
|
if (!dir) {
|
|
@@ -690,6 +785,39 @@ function productName(name) {
|
|
|
690
785
|
return "Craft";
|
|
691
786
|
return /^\d/.test(joined) ? `App${joined}` : joined;
|
|
692
787
|
}
|
|
788
|
+
function writeShell(output, config) {
|
|
789
|
+
if (!config.appName || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(config.bundleId))
|
|
790
|
+
throw new Error("The iOS shell needs an appName and a valid bundleId in craft.config.json.");
|
|
791
|
+
const name = productName(config.appName);
|
|
792
|
+
mkdirSync(join2(output, "Sources"), { recursive: true });
|
|
793
|
+
mkdirSync(join2(output, "Shared"), { recursive: true });
|
|
794
|
+
const swiftTemplate = readFileSync(join2(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
|
|
795
|
+
const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, config.bundleId).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION);
|
|
796
|
+
writeFileSync(join2(output, "Sources", `${name}App.swift`), swiftSource);
|
|
797
|
+
cpSync(join2(TEMPLATES_DIR, "CraftNativeScreen.swift"), join2(output, "Sources", "CraftNativeScreen.swift"));
|
|
798
|
+
cpSync(join2(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join2(output, "Sources", "CraftNativeFlatList.swift"));
|
|
799
|
+
cpSync(join2(TEMPLATES_DIR, "CraftNativeActions.swift"), join2(output, "Sources", "CraftNativeActions.swift"));
|
|
800
|
+
cpSync(join2(TEMPLATES_DIR, "CraftHealth.swift"), join2(output, "Sources", "CraftHealth.swift"));
|
|
801
|
+
cpSync(join2(TEMPLATES_DIR, "CraftMusic.swift"), join2(output, "Sources", "CraftMusic.swift"));
|
|
802
|
+
cpSync(join2(TEMPLATES_DIR, "CraftHeartRate.swift"), join2(output, "Sources", "CraftHeartRate.swift"));
|
|
803
|
+
cpSync(join2(TEMPLATES_DIR, "CraftNativeMutation.swift"), join2(output, "Sources", "CraftNativeMutation.swift"));
|
|
804
|
+
cpSync(join2(TEMPLATES_DIR, "CraftNativeUI.swift"), join2(output, "Sources", "CraftNativeUI.swift"));
|
|
805
|
+
cpSync(join2(TEMPLATES_DIR, "CraftNativeServices.swift"), join2(output, "Sources", "CraftNativeServices.swift"));
|
|
806
|
+
cpSync(join2(TEMPLATES_DIR, "CraftNativeFiles.swift"), join2(output, "Sources", "CraftNativeFiles.swift"));
|
|
807
|
+
cpSync(join2(TEMPLATES_DIR, "CraftWebAudio.swift"), join2(output, "Sources", "CraftWebAudio.swift"));
|
|
808
|
+
cpSync(join2(TEMPLATES_DIR, "CraftHybrid.swift"), join2(output, "Sources", "CraftHybrid.swift"));
|
|
809
|
+
cpSync(join2(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join2(output, "Shared", "CraftActivityAttributes.swift"));
|
|
810
|
+
for (const [enabled, folder, template, filename] of [
|
|
811
|
+
[config.enableLiveActivities, "WidgetExtension", "CraftLiveActivityWidget.swift.template", `${name}LiveActivity.swift`],
|
|
812
|
+
[config.enableWatchApp, "WatchExtension", "CraftWatchApp.swift.template", `${name}WatchApp.swift`]
|
|
813
|
+
]) {
|
|
814
|
+
if (!enabled)
|
|
815
|
+
continue;
|
|
816
|
+
mkdirSync(join2(output, folder), { recursive: true });
|
|
817
|
+
const source = readFileSync(join2(TEMPLATES_DIR, template), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
|
|
818
|
+
writeFileSync(join2(output, folder, filename), source);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
693
821
|
async function init(options) {
|
|
694
822
|
const { name: displayName, bundleId, teamId, output } = options;
|
|
695
823
|
const name = productName(displayName);
|
|
@@ -697,9 +825,9 @@ async function init(options) {
|
|
|
697
825
|
\u26A1 Initializing Craft iOS project: ${displayName}`);
|
|
698
826
|
console.log(` Output: ${output}
|
|
699
827
|
`);
|
|
700
|
-
const dirs = [output,
|
|
828
|
+
const dirs = [output, join2(output, "Sources"), join2(output, "Shared"), join2(output, "dist")];
|
|
701
829
|
if (options.config?.enableWatchApp)
|
|
702
|
-
dirs.push(
|
|
830
|
+
dirs.push(join2(output, "WatchApp"), join2(output, "WatchExtension"));
|
|
703
831
|
for (const dir of dirs) {
|
|
704
832
|
if (!existsSync(dir)) {
|
|
705
833
|
mkdirSync(dir, { recursive: true });
|
|
@@ -726,29 +854,15 @@ async function init(options) {
|
|
|
726
854
|
`));
|
|
727
855
|
if (Object.keys(config.shareStorage ?? {}).length)
|
|
728
856
|
config.enableSecureStorage = true;
|
|
729
|
-
writeFileSync(
|
|
730
|
-
|
|
731
|
-
const
|
|
732
|
-
writeFileSync(join(output, "Sources", `${name}App.swift`), swiftSource);
|
|
733
|
-
cpSync(join(TEMPLATES_DIR, "CraftNativeScreen.swift"), join(output, "Sources", "CraftNativeScreen.swift"));
|
|
734
|
-
cpSync(join(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join(output, "Sources", "CraftNativeFlatList.swift"));
|
|
735
|
-
cpSync(join(TEMPLATES_DIR, "CraftNativeActions.swift"), join(output, "Sources", "CraftNativeActions.swift"));
|
|
736
|
-
cpSync(join(TEMPLATES_DIR, "CraftHealth.swift"), join(output, "Sources", "CraftHealth.swift"));
|
|
737
|
-
cpSync(join(TEMPLATES_DIR, "CraftMusic.swift"), join(output, "Sources", "CraftMusic.swift"));
|
|
738
|
-
cpSync(join(TEMPLATES_DIR, "CraftHeartRate.swift"), join(output, "Sources", "CraftHeartRate.swift"));
|
|
739
|
-
cpSync(join(TEMPLATES_DIR, "CraftNativeMutation.swift"), join(output, "Sources", "CraftNativeMutation.swift"));
|
|
740
|
-
cpSync(join(TEMPLATES_DIR, "CraftNativeUI.swift"), join(output, "Sources", "CraftNativeUI.swift"));
|
|
741
|
-
cpSync(join(TEMPLATES_DIR, "CraftNativeServices.swift"), join(output, "Sources", "CraftNativeServices.swift"));
|
|
742
|
-
cpSync(join(TEMPLATES_DIR, "CraftNativeFiles.swift"), join(output, "Sources", "CraftNativeFiles.swift"));
|
|
743
|
-
cpSync(join(TEMPLATES_DIR, "CraftWebAudio.swift"), join(output, "Sources", "CraftWebAudio.swift"));
|
|
744
|
-
cpSync(join(TEMPLATES_DIR, "CraftHybrid.swift"), join(output, "Sources", "CraftHybrid.swift"));
|
|
745
|
-
const infoPlistTemplate = readFileSync(join(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
|
|
857
|
+
writeFileSync(join2(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
858
|
+
writeShell(output, config);
|
|
859
|
+
const infoPlistTemplate = readFileSync(join2(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
|
|
746
860
|
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>
|
|
747
861
|
<true/>
|
|
748
862
|
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
|
|
749
863
|
<true/>` : "");
|
|
750
|
-
writeFileSync(
|
|
751
|
-
const projectYmlTemplate = readFileSync(
|
|
864
|
+
writeFileSync(join2(output, "Info.plist"), infoPlist);
|
|
865
|
+
const projectYmlTemplate = readFileSync(join2(TEMPLATES_DIR, "project.yml.template"), "utf-8");
|
|
752
866
|
const nativeDependencies = [];
|
|
753
867
|
if (config.enableLiveActivities)
|
|
754
868
|
nativeDependencies.push(` - target: ${name}LiveActivity`);
|
|
@@ -805,40 +919,37 @@ async function init(options) {
|
|
|
805
919
|
if (hasRuntime) {
|
|
806
920
|
console.log(" Linked the Zig runtime from", runtimeDir);
|
|
807
921
|
} else {
|
|
808
|
-
rmSync(
|
|
922
|
+
rmSync(join2(output, "Runtime"), { recursive: true, force: true });
|
|
809
923
|
}
|
|
810
924
|
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:
|
|
811
925
|
${nativeDependencies.join(`
|
|
812
926
|
`)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
|
|
813
927
|
`));
|
|
814
|
-
writeFileSync(
|
|
815
|
-
writeFileSync(
|
|
816
|
-
writeFileSync(
|
|
928
|
+
writeFileSync(join2(output, "project.yml"), projectYml);
|
|
929
|
+
writeFileSync(join2(output, "Craft.entitlements"), renderEntitlements(config));
|
|
930
|
+
writeFileSync(join2(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
|
|
817
931
|
renderAssetCatalog(output, config);
|
|
818
|
-
cpSync(join(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join(output, "Shared", "CraftActivityAttributes.swift"));
|
|
819
932
|
if (config.enableLiveActivities) {
|
|
820
|
-
mkdirSync(
|
|
821
|
-
const
|
|
822
|
-
writeFileSync(
|
|
823
|
-
const widgetInfo = readFileSync(join(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
|
|
824
|
-
writeFileSync(join(output, "WidgetExtension", "Info.plist"), widgetInfo);
|
|
933
|
+
mkdirSync(join2(output, "WidgetExtension"), { recursive: true });
|
|
934
|
+
const widgetInfo = readFileSync(join2(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
|
|
935
|
+
writeFileSync(join2(output, "WidgetExtension", "Info.plist"), widgetInfo);
|
|
825
936
|
}
|
|
826
937
|
if (config.enableWatchApp) {
|
|
827
|
-
const
|
|
828
|
-
writeFileSync(
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
writeFileSync(
|
|
832
|
-
|
|
833
|
-
writeFileSync(join(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
|
|
834
|
-
writeFileSync(join(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
938
|
+
const watchInfo = readFileSync(join2(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
939
|
+
writeFileSync(join2(output, "WatchApp", "Info.plist"), watchInfo);
|
|
940
|
+
writeFileSync(join2(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
941
|
+
const watchExtensionInfo = readFileSync(join2(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
942
|
+
writeFileSync(join2(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
|
|
943
|
+
writeFileSync(join2(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
835
944
|
}
|
|
836
|
-
writeFileSync(
|
|
837
|
-
if (
|
|
945
|
+
writeFileSync(join2(output, "dist", "index.html"), renderOfflinePage(config));
|
|
946
|
+
if (usesNativeBundle(config)) {
|
|
838
947
|
if (config.nativeBundle)
|
|
839
948
|
installNativeBundle(config.nativeBundle, output);
|
|
840
|
-
else
|
|
949
|
+
else if (isHybrid(config))
|
|
841
950
|
console.warn(" \u26A0 nativeScreens is set without nativeBundle: every path opens in the web view until `craft ios build --native-bundle` supplies one.");
|
|
951
|
+
else if (hasNativeRoutes(config))
|
|
952
|
+
console.warn(" \u26A0 nativeScreens is set without nativeBundle: the app has no screens until `craft ios build --native-bundle` supplies one.");
|
|
842
953
|
}
|
|
843
954
|
console.log("\u2705 Project initialized");
|
|
844
955
|
console.log("");
|
|
@@ -858,7 +969,7 @@ async function build(options) {
|
|
|
858
969
|
const { htmlPath, nativeBundlePath, devServer, output, generateProject = true } = options;
|
|
859
970
|
console.log(`
|
|
860
971
|
\uD83D\uDCE6 Building Craft iOS project...`);
|
|
861
|
-
const configPath =
|
|
972
|
+
const configPath = join2(output, "craft.config.json");
|
|
862
973
|
if (!existsSync(configPath)) {
|
|
863
974
|
throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
|
|
864
975
|
}
|
|
@@ -870,12 +981,10 @@ async function build(options) {
|
|
|
870
981
|
if (htmlPath || devServer) {
|
|
871
982
|
throw new Error("A native iOS screen cannot use --html-path or --dev-server.");
|
|
872
983
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
}
|
|
878
|
-
if (!existsSync(join(output, "dist", "native-screen.js"))) {
|
|
984
|
+
const bundle = nativeBundlePath ?? config.nativeBundle;
|
|
985
|
+
if (bundle)
|
|
986
|
+
installNativeBundle(bundle, output);
|
|
987
|
+
if (!existsSync(join2(output, "dist", "native-screen.js"))) {
|
|
879
988
|
throw new Error("Native iOS mode needs dist/native-screen.js. Compile a .stx screen with `stx native compile` first.");
|
|
880
989
|
}
|
|
881
990
|
} else if (nativeBundlePath && !isHybrid(config)) {
|
|
@@ -901,7 +1010,7 @@ async function build(options) {
|
|
|
901
1010
|
if (bundle) {
|
|
902
1011
|
installNativeBundle(bundle, output);
|
|
903
1012
|
console.log(` Native screens: ${bundle} \u2192 dist/native-screen.js`);
|
|
904
|
-
} else if (!existsSync(
|
|
1013
|
+
} else if (!existsSync(join2(output, "dist", "native-screen.js"))) {
|
|
905
1014
|
console.warn(" \u26A0 nativeScreens is set but there is no native bundle: every path opens in the web view.");
|
|
906
1015
|
}
|
|
907
1016
|
}
|
|
@@ -931,7 +1040,7 @@ async function open(options) {
|
|
|
931
1040
|
if (!xcodeproj) {
|
|
932
1041
|
throw new Error(`No Xcode project found in ${output}. Run 'craft ios build' first.`);
|
|
933
1042
|
}
|
|
934
|
-
const projectPath =
|
|
1043
|
+
const projectPath = join2(output, xcodeproj);
|
|
935
1044
|
console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
|
|
936
1045
|
await $`open ${projectPath}`;
|
|
937
1046
|
}
|
|
@@ -972,7 +1081,7 @@ async function showSimulator() {
|
|
|
972
1081
|
const selected = await $`xcode-select -p`.quiet().nothrow();
|
|
973
1082
|
const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
|
|
974
1083
|
if (developer) {
|
|
975
|
-
const app =
|
|
1084
|
+
const app = join2(developer, "Applications", "Simulator.app");
|
|
976
1085
|
if (existsSync(app)) {
|
|
977
1086
|
await $`open ${app}`.quiet().nothrow();
|
|
978
1087
|
return;
|
|
@@ -988,9 +1097,9 @@ async function run(options) {
|
|
|
988
1097
|
if (!xcodeproj) {
|
|
989
1098
|
throw new Error(`No Xcode project found in ${output}`);
|
|
990
1099
|
}
|
|
991
|
-
const projectPath =
|
|
1100
|
+
const projectPath = join2(output, xcodeproj);
|
|
992
1101
|
const appName = xcodeproj.replace(".xcodeproj", "");
|
|
993
|
-
const configPath =
|
|
1102
|
+
const configPath = join2(output, "craft.config.json");
|
|
994
1103
|
if (!existsSync(configPath))
|
|
995
1104
|
throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
|
|
996
1105
|
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
@@ -1003,8 +1112,8 @@ async function run(options) {
|
|
|
1003
1112
|
console.log(` Device: ${device.name} (${device.runtime})`);
|
|
1004
1113
|
try {
|
|
1005
1114
|
await bootSimulator(device);
|
|
1006
|
-
const derivedData =
|
|
1007
|
-
const product =
|
|
1115
|
+
const derivedData = join2(output, "build");
|
|
1116
|
+
const product = join2(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
|
|
1008
1117
|
await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
|
|
1009
1118
|
if (!existsSync(product)) {
|
|
1010
1119
|
throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
|
package/dist/index.d.ts
CHANGED
|
@@ -83,6 +83,14 @@ export interface CraftConfig {
|
|
|
83
83
|
* Each replaces the generic sentence Craft writes for that key.
|
|
84
84
|
*/
|
|
85
85
|
usageDescriptions?: Record<string, string>;
|
|
86
|
+
/**
|
|
87
|
+
* The app's site. With `renderer: "web"` it is the page the web view loads.
|
|
88
|
+
* With `renderer: "native"` and `nativeScreens` there is no web view: its
|
|
89
|
+
* path is the path the app starts on (`/` when unset), and a path no native
|
|
90
|
+
* screen draws (`craft.navigation.open('/terms')`, or a link to this host)
|
|
91
|
+
* opens in the in-app browser against its origin. Native screens read it as
|
|
92
|
+
* `__stxNativeBridge.appURL`.
|
|
93
|
+
*/
|
|
86
94
|
devServerURL?: string;
|
|
87
95
|
iosVersion?: string;
|
|
88
96
|
watchosVersion?: string;
|
|
@@ -168,16 +176,32 @@ export interface CraftConfig {
|
|
|
168
176
|
orientations?: Array<'portrait' | 'landscape-left' | 'landscape-right' | 'portrait-upside-down'>;
|
|
169
177
|
deviceFamilies?: Array<'iphone' | 'ipad'>;
|
|
170
178
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
179
|
+
* App paths drawn as native stx screens, each to the screen in the native
|
|
180
|
+
* bundle that draws it, e.g. `{ "/m": "Today", "/m/calendar": "Calendar" }`.
|
|
181
|
+
* A segment may be a parameter (`/m/workout/:id`) and the last one `*`.
|
|
182
|
+
* Needs `nativeBundle`.
|
|
183
|
+
*
|
|
184
|
+
* With `renderer: "web"` (hybrid) every other path stays in the web view,
|
|
185
|
+
* and a path whose screen is missing or throws falls back to it.
|
|
186
|
+
*
|
|
187
|
+
* With `renderer: "native"` the app is all native, with the same tabs and
|
|
188
|
+
* path routing and no web view at all: every tab id and the start path (the
|
|
189
|
+
* path of `devServerURL`) must be a native screen's, and any other path
|
|
190
|
+
* opens in the in-app browser on the app's site.
|
|
177
191
|
*/
|
|
178
192
|
nativeScreens?: Record<string, string>;
|
|
179
|
-
/**
|
|
193
|
+
/** The bundle `stx native compile --format bundle` wrote, copied to `dist/native-screen.js`. */
|
|
180
194
|
nativeBundle?: string;
|
|
195
|
+
/**
|
|
196
|
+
* The bundle route of a screen-less script that runs once for the app's
|
|
197
|
+
* whole life, beside the screens (Stacks names it `__app`). It hears the
|
|
198
|
+
* app's events (`craftNotificationResponse`, `craftNotificationReceived`,
|
|
199
|
+
* `craftSilentPush`, `craftBackgroundRefresh`, `craftShortcut`,
|
|
200
|
+
* `craftSiriShortcut`) as `CRAFT_EVENT` messages instead of the page, every
|
|
201
|
+
* `APP_STATE`, and can open paths with `craft.navigation.open`. Needs
|
|
202
|
+
* `nativeScreens` or `renderer: "native"`, and the native bundle.
|
|
203
|
+
*/
|
|
204
|
+
nativeApp?: string;
|
|
181
205
|
/**
|
|
182
206
|
* The native tab bar, shown from the first frame, before the page loads.
|
|
183
207
|
* The page's own `tabBar.set` replaces it later. Each `id` is the tab's
|
|
@@ -214,8 +238,18 @@ export interface NativePathMatch {
|
|
|
214
238
|
export declare function matchNativePath(nativeScreens: Record<string, string> | undefined, raw: string): NativePathMatch | null;
|
|
215
239
|
/** Whether a config mixes native screens into its web page. */
|
|
216
240
|
export declare function isHybrid(config: Pick<CraftConfig, 'renderer' | 'nativeScreens'>): boolean;
|
|
241
|
+
/** Whether a config is an all-native app with routes: `renderer: "native"` and `nativeScreens`, tabs and paths with no web view. */
|
|
242
|
+
export declare function hasNativeRoutes(config: Pick<CraftConfig, 'renderer' | 'nativeScreens'>): boolean;
|
|
243
|
+
/** Whether a config draws any native screen, and so needs `dist/native-screen.js`. */
|
|
244
|
+
export declare function usesNativeBundle(config: Pick<CraftConfig, 'renderer' | 'nativeScreens'>): boolean;
|
|
245
|
+
/**
|
|
246
|
+
* The path the app starts on: the path of `devServerURL` (with its query),
|
|
247
|
+
* `/` when there is none. The shell reads it the same way
|
|
248
|
+
* (`CraftHybridRoutes.appPath`).
|
|
249
|
+
*/
|
|
250
|
+
export declare function nativeStartPath(config: Pick<CraftConfig, 'devServerURL'>): string;
|
|
217
251
|
/** What is wrong with a config's hybrid and tab settings, one sentence each. */
|
|
218
|
-
export declare function hybridConfigProblems(config: Pick<CraftConfig, 'renderer' | 'nativeScreens' | 'tabs' | 'shareStorage'>): string[];
|
|
252
|
+
export declare function hybridConfigProblems(config: Pick<CraftConfig, 'renderer' | 'nativeScreens' | 'tabs' | 'shareStorage' | 'devServerURL' | 'nativeApp'>): string[];
|
|
219
253
|
export interface CraftBackgroundRefresh {
|
|
220
254
|
enabled: boolean;
|
|
221
255
|
/** The BGTaskScheduler identifier. Default `<bundleId>.refresh`. */
|
|
@@ -412,6 +446,8 @@ export declare function installRuntime(output: string, runtimeDir: string): Prom
|
|
|
412
446
|
* valid Swift type.
|
|
413
447
|
*/
|
|
414
448
|
export declare function productName(name: string): string;
|
|
449
|
+
/** Restore package-owned Swift into an existing project, without changing its Xcode metadata or bundled screens. */
|
|
450
|
+
export declare function restoreShell(output: string): void;
|
|
415
451
|
export declare function init(options: InitOptions): Promise<void>;
|
|
416
452
|
/**
|
|
417
453
|
* Build web assets and generate Xcode project
|
|
@@ -475,3 +511,5 @@ export declare function bootSimulator(device: SimulatorDevice): Promise<void>;
|
|
|
475
511
|
*/
|
|
476
512
|
export declare function showSimulator(): Promise<void>;
|
|
477
513
|
export declare function run(options: RunOptions): Promise<void>;
|
|
514
|
+
export { createNativeMutationValidator, initSimulatorDriver, serializeSimulatorSteps } from './testing.js';
|
|
515
|
+
export type { NativeMutationValidator, NativeValidationResult, SimulatorDriverOptions, SimulatorStep } from './testing.js';
|