craft-native 0.0.164 → 0.0.167
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android/templates/AndroidManifest.xml.template +1 -1
- package/dist/android/templates/MainActivityNative.kt.template +416 -36
- package/dist/api/mobile.d.ts +25 -1
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +1 -0
- package/dist/ios/src/index.js +138 -88
- package/dist/ios/src/source-audit.d.ts +8 -0
- package/dist/ios/templates/CraftApp.swift +102 -13
- package/dist/ios/templates/CraftHealth.swift +214 -39
- package/dist/ios/templates/CraftHeartRate.swift +34 -4
- package/dist/ios/templates/CraftHybrid.swift +140 -17
- package/dist/ios/templates/CraftNativeScreen.swift +338 -61
- package/dist/ios/templates/CraftNativeServices.swift +76 -23
- package/dist/mobile.js +29 -0
- package/package.json +1 -1
package/dist/api/mobile.d.ts
CHANGED
|
@@ -1105,6 +1105,8 @@ export interface NativeTabBarOptions {
|
|
|
1105
1105
|
/** The current tab's colour, as `#rrggbb`; the system's label colour if unset. */
|
|
1106
1106
|
tint?: string;
|
|
1107
1107
|
tintDark?: string;
|
|
1108
|
+
/** Whether Craft's horizontal tab-switch gesture may begin over the page. Defaults to true. */
|
|
1109
|
+
swipeEnabled?: boolean;
|
|
1108
1110
|
}
|
|
1109
1111
|
/**
|
|
1110
1112
|
* The phone's own tab bar, floating over the page: Liquid Glass on iOS 26 and
|
|
@@ -1535,8 +1537,12 @@ export interface HealthMindfulSample {
|
|
|
1535
1537
|
externalId?: string;
|
|
1536
1538
|
deviceName?: string;
|
|
1537
1539
|
}
|
|
1540
|
+
export interface HealthHeartRateUpdatesOptions {
|
|
1541
|
+
/** Epoch milliseconds: samples that start at or after this. Defaults to two minutes ago. */
|
|
1542
|
+
since?: number;
|
|
1543
|
+
}
|
|
1538
1544
|
/** The health reads and writes a host may or may not have, depending on the app build it runs in. */
|
|
1539
|
-
export type HealthReadMethod = 'getWorkouts' | 'getDailyStatistics' | 'getHeartRateSamples' | 'getSleepIntervals' | 'saveData' | 'saveMindfulSession' | 'getMindfulSessions';
|
|
1545
|
+
export type HealthReadMethod = 'getWorkouts' | 'getDailyStatistics' | 'getHeartRateSamples' | 'getSleepIntervals' | 'saveData' | 'saveMindfulSession' | 'getMindfulSessions' | 'startHeartRateUpdates' | 'stopHeartRateUpdates';
|
|
1540
1546
|
export declare const health: {
|
|
1541
1547
|
/**
|
|
1542
1548
|
* Whether this app build answers `method`. An app installed before a read
|
|
@@ -1569,6 +1575,24 @@ export declare const health: {
|
|
|
1569
1575
|
saveMindfulSession(session: HealthMindfulSession): Promise<HealthMindfulSessionResult>;
|
|
1570
1576
|
/** Mindful sessions between two instants, oldest first, each marked `ownApp` when this app wrote it. iOS only for now. */
|
|
1571
1577
|
getMindfulSessions(options?: HealthMindfulSessionQuery): Promise<HealthMindfulSample[]>;
|
|
1578
|
+
/**
|
|
1579
|
+
* Heart rate as Apple Health receives it, until `stopHeartRateUpdates()`:
|
|
1580
|
+
* the samples since `since` (two minutes ago by default) first, then each
|
|
1581
|
+
* batch as the watch syncs it, to every `onHeartRate` listener. Needs the
|
|
1582
|
+
* `heartRate` grant. One stream at a time; starting again replaces it.
|
|
1583
|
+
* HealthKit only has what the watch has synced: every few seconds during a
|
|
1584
|
+
* workout or Mindfulness session on the watch, every few minutes otherwise.
|
|
1585
|
+
* iOS only for now.
|
|
1586
|
+
*/
|
|
1587
|
+
startHeartRateUpdates(options?: HealthHeartRateUpdatesOptions): Promise<boolean>;
|
|
1588
|
+
/** Stops the stream `startHeartRateUpdates` started; true also when none ran. */
|
|
1589
|
+
stopHeartRateUpdates(): Promise<boolean>;
|
|
1590
|
+
/**
|
|
1591
|
+
* Each batch of new heart-rate samples while `startHeartRateUpdates` runs,
|
|
1592
|
+
* oldest first, every sample once. Returns the unsubscribe; off a phone, or
|
|
1593
|
+
* in a build without the stream, it never calls back.
|
|
1594
|
+
*/
|
|
1595
|
+
onHeartRate(callback: (samples: HealthHeartRateSample[]) => void): () => void;
|
|
1572
1596
|
};
|
|
1573
1597
|
export interface LiveActivityState {
|
|
1574
1598
|
status?: string;
|
package/dist/cli.js
CHANGED
|
@@ -3303,7 +3303,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3303
3303
|
`);
|
|
3304
3304
|
}
|
|
3305
3305
|
// package.json
|
|
3306
|
-
var version = "0.0.
|
|
3306
|
+
var version = "0.0.167";
|
|
3307
3307
|
|
|
3308
3308
|
// bin/cli.ts
|
|
3309
3309
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
|
@@ -40,9 +40,9 @@ export interface FlexStyle {
|
|
|
40
40
|
flexWrap?: 'nowrap' | 'wrap' | 'wrap-reverse';
|
|
41
41
|
/** Justify content (main axis) */
|
|
42
42
|
justifyContent?: 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly';
|
|
43
|
-
/** Align items (cross axis) */
|
|
43
|
+
/** Align items (cross axis); baseline aligns text-bearing children by their first baseline */
|
|
44
44
|
alignItems?: 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
|
|
45
|
-
/** Align self (override parent alignItems) */
|
|
45
|
+
/** Align self (override parent alignItems), including first-baseline alignment */
|
|
46
46
|
alignSelf?: 'auto' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
|
|
47
47
|
/** Align content (multi-line) */
|
|
48
48
|
alignContent?: 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'space-between' | 'space-around' | 'space-evenly';
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/dist/ios/src/index.d.ts
CHANGED
|
@@ -477,6 +477,7 @@ export declare function installRuntime(output: string, runtimeDir: string): Prom
|
|
|
477
477
|
export declare function productName(name: string): string;
|
|
478
478
|
/** Restore package-owned Swift into an existing project, without changing its Xcode metadata or bundled screens. */
|
|
479
479
|
export declare function restoreShell(output: string): void;
|
|
480
|
+
export { assertNoApplicationSwiftSources, findApplicationSwiftSources } from './source-audit.js';
|
|
480
481
|
export declare function init(options: InitOptions): Promise<void>;
|
|
481
482
|
/**
|
|
482
483
|
* Build web assets and generate Xcode project
|
package/dist/ios/src/index.js
CHANGED
|
@@ -1,15 +1,63 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../ios/src/index.ts
|
|
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
|
|
3
|
+
import { cpSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
4
|
+
import { dirname as dirname2, join as join3, resolve } from "path";
|
|
5
5
|
var {$ } = globalThis.Bun;
|
|
6
6
|
|
|
7
|
+
// ../ios/src/source-audit.ts
|
|
8
|
+
import { readdirSync } from "fs";
|
|
9
|
+
import { join, relative, sep } from "path";
|
|
10
|
+
var defaultIgnoredPaths = [
|
|
11
|
+
".git",
|
|
12
|
+
"node_modules",
|
|
13
|
+
"dist",
|
|
14
|
+
"build",
|
|
15
|
+
"storage/framework/mobile"
|
|
16
|
+
];
|
|
17
|
+
function normalisePath(path) {
|
|
18
|
+
return path.split(sep).join("/");
|
|
19
|
+
}
|
|
20
|
+
function isIgnored(path, ignoredPaths) {
|
|
21
|
+
const normalised = normalisePath(path);
|
|
22
|
+
return ignoredPaths.some((ignored) => {
|
|
23
|
+
const prefix = normalisePath(ignored).replace(/\/+$/, "");
|
|
24
|
+
return normalised === prefix || normalised.startsWith(`${prefix}/`);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function visit(root, current, ignoredPaths, found) {
|
|
28
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
29
|
+
const absolute = join(current, entry.name);
|
|
30
|
+
const relativePath = relative(root, absolute);
|
|
31
|
+
if (isIgnored(relativePath, ignoredPaths))
|
|
32
|
+
continue;
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
visit(root, absolute, ignoredPaths, found);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (entry.isFile() && entry.name.endsWith(".swift"))
|
|
38
|
+
found.push(normalisePath(relativePath));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function findApplicationSwiftSources(root, options = {}) {
|
|
42
|
+
const ignoredPaths = [...defaultIgnoredPaths, ...options.ignoredPaths ?? []];
|
|
43
|
+
const found = [];
|
|
44
|
+
visit(root, root, ignoredPaths, found);
|
|
45
|
+
return found.sort();
|
|
46
|
+
}
|
|
47
|
+
function assertNoApplicationSwiftSources(root, options = {}) {
|
|
48
|
+
const sources = findApplicationSwiftSources(root, options);
|
|
49
|
+
if (sources.length > 0) {
|
|
50
|
+
throw new Error(`Application-owned Swift sources found:
|
|
51
|
+
${sources.map((path) => `- ${path}`).join(`
|
|
52
|
+
`)}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
7
55
|
// ../ios/src/testing.ts
|
|
8
56
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
9
57
|
import { tmpdir } from "os";
|
|
10
|
-
import { dirname, join } from "path";
|
|
58
|
+
import { dirname, join as join2 } from "path";
|
|
11
59
|
import { spawnSync } from "child_process";
|
|
12
|
-
var templates =
|
|
60
|
+
var templates = join2(dirname(import.meta.dir), "templates/testing");
|
|
13
61
|
function bundleIdentifier(value) {
|
|
14
62
|
if (!/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(value))
|
|
15
63
|
throw new Error(`Invalid bundle identifier: ${value}`);
|
|
@@ -44,9 +92,9 @@ function initSimulatorDriver(options) {
|
|
|
44
92
|
["DriverTests.swift.template", "UITests/DriverTests.swift"],
|
|
45
93
|
["project.yml.template", "project.yml"]
|
|
46
94
|
]) {
|
|
47
|
-
const path =
|
|
95
|
+
const path = join2(output, destination);
|
|
48
96
|
mkdirSync(dirname(path), { recursive: true });
|
|
49
|
-
const content = readFileSync(
|
|
97
|
+
const content = readFileSync(join2(templates, source), "utf8").replace(/\{\{([A-Z_]+)\}\}/g, (_, key) => replacements[key]);
|
|
50
98
|
let previous;
|
|
51
99
|
try {
|
|
52
100
|
previous = readFileSync(path, "utf8");
|
|
@@ -108,11 +156,11 @@ function serializeSimulatorSteps(steps) {
|
|
|
108
156
|
`;
|
|
109
157
|
}
|
|
110
158
|
function createNativeMutationValidator() {
|
|
111
|
-
const output = mkdtempSync(
|
|
112
|
-
const binary =
|
|
159
|
+
const output = mkdtempSync(join2(tmpdir(), "craft-native-mutation-"));
|
|
160
|
+
const binary = join2(output, "validate");
|
|
113
161
|
try {
|
|
114
|
-
writeFileSync(
|
|
115
|
-
const build = Bun.spawnSync(["swiftc",
|
|
162
|
+
writeFileSync(join2(output, "main.swift"), readFileSync(join2(templates, "MutationValidator.swift")));
|
|
163
|
+
const build = Bun.spawnSync(["swiftc", join2(templates, "../CraftNativeMutation.swift"), join2(output, "main.swift"), "-o", binary]);
|
|
116
164
|
if (build.exitCode !== 0)
|
|
117
165
|
throw new Error(build.stderr.toString());
|
|
118
166
|
} catch (error) {
|
|
@@ -124,9 +172,9 @@ function createNativeMutationValidator() {
|
|
|
124
172
|
validate(messages) {
|
|
125
173
|
if (disposed)
|
|
126
174
|
throw new Error("The native mutation validator has been disposed");
|
|
127
|
-
const input =
|
|
175
|
+
const input = join2(output, "messages.json");
|
|
128
176
|
writeFileSync(input, JSON.stringify(messages));
|
|
129
|
-
const report =
|
|
177
|
+
const report = join2(output, "result.txt");
|
|
130
178
|
rmSync(report, { force: true });
|
|
131
179
|
const result = spawnSync(binary, [input, report], { encoding: "utf8" });
|
|
132
180
|
let stdout = result.stdout ?? "";
|
|
@@ -143,12 +191,12 @@ function createNativeMutationValidator() {
|
|
|
143
191
|
}
|
|
144
192
|
|
|
145
193
|
// ../ios/src/index.ts
|
|
146
|
-
var TEMPLATES_DIR =
|
|
194
|
+
var TEMPLATES_DIR = join3(dirname2(import.meta.dir), "templates");
|
|
147
195
|
function resolveCraftVersion(startDir = import.meta.dir) {
|
|
148
196
|
let directory = resolve(startDir);
|
|
149
197
|
while (true) {
|
|
150
198
|
try {
|
|
151
|
-
const packageJSON = JSON.parse(readFileSync2(
|
|
199
|
+
const packageJSON = JSON.parse(readFileSync2(join3(directory, "package.json"), "utf8"));
|
|
152
200
|
if (packageJSON.name === "@craft-native/ios" && typeof packageJSON.version === "string" && packageJSON.version.length > 0)
|
|
153
201
|
return packageJSON.version;
|
|
154
202
|
} catch {}
|
|
@@ -300,7 +348,7 @@ function hybridConfigProblems(config) {
|
|
|
300
348
|
function installNativeBundle(source, output) {
|
|
301
349
|
if (!existsSync(source))
|
|
302
350
|
throw new Error(`Native bundle not found: ${source}`);
|
|
303
|
-
const destination =
|
|
351
|
+
const destination = join3(output, "dist", "native-screen.js");
|
|
304
352
|
mkdirSync2(dirname2(destination), { recursive: true });
|
|
305
353
|
if (resolve(source) !== resolve(destination))
|
|
306
354
|
cpSync(source, destination);
|
|
@@ -676,20 +724,20 @@ function writeOpaqueIcon(source, target) {
|
|
|
676
724
|
}
|
|
677
725
|
}
|
|
678
726
|
function renderAssetCatalog(output, config) {
|
|
679
|
-
const catalog =
|
|
680
|
-
const appIcon =
|
|
681
|
-
const launchBackground =
|
|
727
|
+
const catalog = join3(output, "Assets.xcassets");
|
|
728
|
+
const appIcon = join3(catalog, "AppIcon.appiconset");
|
|
729
|
+
const launchBackground = join3(catalog, "LaunchBackground.colorset");
|
|
682
730
|
mkdirSync2(appIcon, { recursive: true });
|
|
683
731
|
mkdirSync2(launchBackground, { recursive: true });
|
|
684
|
-
writeFileSync2(
|
|
732
|
+
writeFileSync2(join3(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
|
|
685
733
|
`);
|
|
686
734
|
const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
|
|
687
735
|
if (config.appIconPath) {
|
|
688
736
|
if (!existsSync(config.appIconPath))
|
|
689
737
|
throw new Error(`App icon not found: ${config.appIconPath}`);
|
|
690
|
-
writeOpaqueIcon(config.appIconPath,
|
|
738
|
+
writeOpaqueIcon(config.appIconPath, join3(appIcon, iconFilename));
|
|
691
739
|
}
|
|
692
|
-
writeFileSync2(
|
|
740
|
+
writeFileSync2(join3(appIcon, "Contents.json"), `${JSON.stringify({
|
|
693
741
|
images: iconFilename ? [{
|
|
694
742
|
filename: iconFilename,
|
|
695
743
|
idiom: "universal",
|
|
@@ -716,7 +764,7 @@ function renderAssetCatalog(output, config) {
|
|
|
716
764
|
idiom: "universal"
|
|
717
765
|
});
|
|
718
766
|
}
|
|
719
|
-
writeFileSync2(
|
|
767
|
+
writeFileSync2(join3(launchBackground, "Contents.json"), `${JSON.stringify({
|
|
720
768
|
colors,
|
|
721
769
|
info: { author: "xcode", version: 1 }
|
|
722
770
|
}, null, 2)}
|
|
@@ -725,7 +773,7 @@ function renderAssetCatalog(output, config) {
|
|
|
725
773
|
}
|
|
726
774
|
var LAUNCH_LOGO = "LaunchLogo";
|
|
727
775
|
function renderLaunchLogo(catalog, config) {
|
|
728
|
-
const imageset =
|
|
776
|
+
const imageset = join3(catalog, `${LAUNCH_LOGO}.imageset`);
|
|
729
777
|
rmSync2(imageset, { recursive: true, force: true });
|
|
730
778
|
if (!config.splashImagePath)
|
|
731
779
|
return;
|
|
@@ -741,14 +789,14 @@ function renderLaunchLogo(catalog, config) {
|
|
|
741
789
|
if (extension === "svg" || extension === "pdf")
|
|
742
790
|
vector = true;
|
|
743
791
|
const filename = `${LAUNCH_LOGO}${dark ? "-dark" : ""}.${extension}`;
|
|
744
|
-
cpSync(source,
|
|
792
|
+
cpSync(source, join3(imageset, filename));
|
|
745
793
|
images.push({
|
|
746
794
|
...dark ? { appearances: [{ appearance: "luminosity", value: "dark" }] } : {},
|
|
747
795
|
filename,
|
|
748
796
|
idiom: "universal"
|
|
749
797
|
});
|
|
750
798
|
}
|
|
751
|
-
writeFileSync2(
|
|
799
|
+
writeFileSync2(join3(imageset, "Contents.json"), `${JSON.stringify({
|
|
752
800
|
images,
|
|
753
801
|
info: { author: "xcode", version: 1 },
|
|
754
802
|
...vector ? { properties: { "preserves-vector-representation": true } } : {}
|
|
@@ -862,15 +910,15 @@ function syncWebAssets(source, output) {
|
|
|
862
910
|
const sourcePath = resolve(source);
|
|
863
911
|
if (!existsSync(sourcePath))
|
|
864
912
|
throw new Error(`Web asset path not found: ${source}`);
|
|
865
|
-
const distDir =
|
|
913
|
+
const distDir = join3(output, "dist");
|
|
866
914
|
rmSync2(distDir, { recursive: true, force: true });
|
|
867
915
|
mkdirSync2(distDir, { recursive: true });
|
|
868
916
|
if (statSync(sourcePath).isDirectory()) {
|
|
869
917
|
cpSync(sourcePath, distDir, { recursive: true });
|
|
870
918
|
} else {
|
|
871
|
-
cpSync(sourcePath,
|
|
919
|
+
cpSync(sourcePath, join3(distDir, "index.html"));
|
|
872
920
|
}
|
|
873
|
-
if (!existsSync(
|
|
921
|
+
if (!existsSync(join3(distDir, "index.html"))) {
|
|
874
922
|
throw new Error(`Web asset directory must contain index.html: ${source}`);
|
|
875
923
|
}
|
|
876
924
|
}
|
|
@@ -892,32 +940,32 @@ var RUNTIME_ARCHIVES = {
|
|
|
892
940
|
};
|
|
893
941
|
async function installRuntime(output, runtimeDir) {
|
|
894
942
|
const resolved = Object.entries(RUNTIME_ARCHIVES).map(([sdk, archives]) => {
|
|
895
|
-
const present = archives.filter((a) => existsSync(
|
|
943
|
+
const present = archives.filter((a) => existsSync(join3(runtimeDir, a)));
|
|
896
944
|
if (present.length === 0) {
|
|
897
945
|
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.`);
|
|
898
946
|
}
|
|
899
947
|
return { sdk, archives, present };
|
|
900
948
|
});
|
|
901
|
-
const dest =
|
|
949
|
+
const dest = join3(output, "Runtime");
|
|
902
950
|
rmSync2(dest, { recursive: true, force: true });
|
|
903
951
|
for (const { sdk, archives, present } of resolved) {
|
|
904
|
-
const sdkDir =
|
|
952
|
+
const sdkDir = join3(dest, sdk);
|
|
905
953
|
mkdirSync2(sdkDir, { recursive: true });
|
|
906
|
-
const target =
|
|
954
|
+
const target = join3(sdkDir, "libcraft-ios.a");
|
|
907
955
|
if (present.length === 1) {
|
|
908
956
|
const missing = archives.filter((a) => !present.includes(a));
|
|
909
957
|
if (missing.length > 0) {
|
|
910
958
|
console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
|
|
911
959
|
}
|
|
912
|
-
cpSync(
|
|
960
|
+
cpSync(join3(runtimeDir, present[0]), target);
|
|
913
961
|
} else {
|
|
914
|
-
await $`lipo -create ${present.map((a) =>
|
|
962
|
+
await $`lipo -create ${present.map((a) => join3(runtimeDir, a))} -output ${target}`.quiet();
|
|
915
963
|
}
|
|
916
964
|
}
|
|
917
965
|
return true;
|
|
918
966
|
}
|
|
919
967
|
async function refreshRuntime(output, override) {
|
|
920
|
-
if (!existsSync(
|
|
968
|
+
if (!existsSync(join3(output, "Runtime")))
|
|
921
969
|
return;
|
|
922
970
|
const dir = resolveRuntimeDir(override);
|
|
923
971
|
if (!dir) {
|
|
@@ -935,41 +983,41 @@ function productName(name) {
|
|
|
935
983
|
return /^\d/.test(joined) ? `App${joined}` : joined;
|
|
936
984
|
}
|
|
937
985
|
function restoreShell(output) {
|
|
938
|
-
const config = JSON.parse(readFileSync2(
|
|
986
|
+
const config = JSON.parse(readFileSync2(join3(output, "craft.config.json"), "utf8"));
|
|
939
987
|
writeShell(output, config);
|
|
940
988
|
}
|
|
941
989
|
function writeShell(output, config) {
|
|
942
990
|
if (!config.appName || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(config.bundleId))
|
|
943
991
|
throw new Error("The iOS shell needs an appName and a valid bundleId in craft.config.json.");
|
|
944
992
|
const name = productName(config.appName);
|
|
945
|
-
mkdirSync2(
|
|
946
|
-
mkdirSync2(
|
|
947
|
-
const swiftTemplate = readFileSync2(
|
|
993
|
+
mkdirSync2(join3(output, "Sources"), { recursive: true });
|
|
994
|
+
mkdirSync2(join3(output, "Shared"), { recursive: true });
|
|
995
|
+
const swiftTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
|
|
948
996
|
const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, config.bundleId).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION);
|
|
949
|
-
writeFileSync2(
|
|
950
|
-
cpSync(
|
|
951
|
-
cpSync(
|
|
952
|
-
cpSync(
|
|
953
|
-
cpSync(
|
|
954
|
-
cpSync(
|
|
955
|
-
cpSync(
|
|
956
|
-
cpSync(
|
|
957
|
-
cpSync(
|
|
958
|
-
cpSync(
|
|
959
|
-
cpSync(
|
|
960
|
-
cpSync(
|
|
961
|
-
cpSync(
|
|
962
|
-
cpSync(
|
|
963
|
-
cpSync(
|
|
997
|
+
writeFileSync2(join3(output, "Sources", `${name}App.swift`), swiftSource);
|
|
998
|
+
cpSync(join3(TEMPLATES_DIR, "CraftNativeScreen.swift"), join3(output, "Sources", "CraftNativeScreen.swift"));
|
|
999
|
+
cpSync(join3(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join3(output, "Sources", "CraftNativeFlatList.swift"));
|
|
1000
|
+
cpSync(join3(TEMPLATES_DIR, "CraftNativeActions.swift"), join3(output, "Sources", "CraftNativeActions.swift"));
|
|
1001
|
+
cpSync(join3(TEMPLATES_DIR, "CraftHealth.swift"), join3(output, "Sources", "CraftHealth.swift"));
|
|
1002
|
+
cpSync(join3(TEMPLATES_DIR, "CraftMusic.swift"), join3(output, "Sources", "CraftMusic.swift"));
|
|
1003
|
+
cpSync(join3(TEMPLATES_DIR, "CraftHeartRate.swift"), join3(output, "Sources", "CraftHeartRate.swift"));
|
|
1004
|
+
cpSync(join3(TEMPLATES_DIR, "CraftScheduledWorkouts.swift"), join3(output, "Sources", "CraftScheduledWorkouts.swift"));
|
|
1005
|
+
cpSync(join3(TEMPLATES_DIR, "CraftNativeMutation.swift"), join3(output, "Sources", "CraftNativeMutation.swift"));
|
|
1006
|
+
cpSync(join3(TEMPLATES_DIR, "CraftNativeUI.swift"), join3(output, "Sources", "CraftNativeUI.swift"));
|
|
1007
|
+
cpSync(join3(TEMPLATES_DIR, "CraftNativeServices.swift"), join3(output, "Sources", "CraftNativeServices.swift"));
|
|
1008
|
+
cpSync(join3(TEMPLATES_DIR, "CraftNativeFiles.swift"), join3(output, "Sources", "CraftNativeFiles.swift"));
|
|
1009
|
+
cpSync(join3(TEMPLATES_DIR, "CraftWebAudio.swift"), join3(output, "Sources", "CraftWebAudio.swift"));
|
|
1010
|
+
cpSync(join3(TEMPLATES_DIR, "CraftHybrid.swift"), join3(output, "Sources", "CraftHybrid.swift"));
|
|
1011
|
+
cpSync(join3(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join3(output, "Shared", "CraftActivityAttributes.swift"));
|
|
964
1012
|
for (const [enabled, folder, template, filename] of [
|
|
965
1013
|
[config.enableLiveActivities, "WidgetExtension", "CraftLiveActivityWidget.swift.template", `${name}LiveActivity.swift`],
|
|
966
1014
|
[config.enableWatchApp, "WatchExtension", "CraftWatchApp.swift.template", `${name}WatchApp.swift`]
|
|
967
1015
|
]) {
|
|
968
1016
|
if (!enabled)
|
|
969
1017
|
continue;
|
|
970
|
-
mkdirSync2(
|
|
971
|
-
const source = readFileSync2(
|
|
972
|
-
writeFileSync2(
|
|
1018
|
+
mkdirSync2(join3(output, folder), { recursive: true });
|
|
1019
|
+
const source = readFileSync2(join3(TEMPLATES_DIR, template), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
|
|
1020
|
+
writeFileSync2(join3(output, folder, filename), source);
|
|
973
1021
|
}
|
|
974
1022
|
}
|
|
975
1023
|
async function init(options) {
|
|
@@ -979,9 +1027,9 @@ async function init(options) {
|
|
|
979
1027
|
\u26A1 Initializing Craft iOS project: ${displayName}`);
|
|
980
1028
|
console.log(` Output: ${output}
|
|
981
1029
|
`);
|
|
982
|
-
const dirs = [output,
|
|
1030
|
+
const dirs = [output, join3(output, "Sources"), join3(output, "Shared"), join3(output, "dist")];
|
|
983
1031
|
if (options.config?.enableWatchApp)
|
|
984
|
-
dirs.push(
|
|
1032
|
+
dirs.push(join3(output, "WatchApp"), join3(output, "WatchExtension"));
|
|
985
1033
|
for (const dir of dirs) {
|
|
986
1034
|
if (!existsSync(dir)) {
|
|
987
1035
|
mkdirSync2(dir, { recursive: true });
|
|
@@ -1008,15 +1056,15 @@ async function init(options) {
|
|
|
1008
1056
|
`));
|
|
1009
1057
|
if (Object.keys(config.shareStorage ?? {}).length)
|
|
1010
1058
|
config.enableSecureStorage = true;
|
|
1011
|
-
writeFileSync2(
|
|
1059
|
+
writeFileSync2(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
1012
1060
|
writeShell(output, config);
|
|
1013
|
-
const infoPlistTemplate = readFileSync2(
|
|
1061
|
+
const infoPlistTemplate = readFileSync2(join3(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
|
|
1014
1062
|
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>
|
|
1015
1063
|
<true/>
|
|
1016
1064
|
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
|
|
1017
1065
|
<true/>` : "");
|
|
1018
|
-
writeFileSync2(
|
|
1019
|
-
const projectYmlTemplate = readFileSync2(
|
|
1066
|
+
writeFileSync2(join3(output, "Info.plist"), infoPlist);
|
|
1067
|
+
const projectYmlTemplate = readFileSync2(join3(TEMPLATES_DIR, "project.yml.template"), "utf-8");
|
|
1020
1068
|
const nativeDependencies = [];
|
|
1021
1069
|
if (config.enableLiveActivities)
|
|
1022
1070
|
nativeDependencies.push(` - target: ${name}LiveActivity`);
|
|
@@ -1073,30 +1121,30 @@ async function init(options) {
|
|
|
1073
1121
|
if (hasRuntime) {
|
|
1074
1122
|
console.log(" Linked the Zig runtime from", runtimeDir);
|
|
1075
1123
|
} else {
|
|
1076
|
-
rmSync2(
|
|
1124
|
+
rmSync2(join3(output, "Runtime"), { recursive: true, force: true });
|
|
1077
1125
|
}
|
|
1078
1126
|
const projectYml = projectYmlTemplate.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:
|
|
1079
1127
|
${nativeDependencies.join(`
|
|
1080
1128
|
`)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
|
|
1081
1129
|
`));
|
|
1082
|
-
writeFileSync2(
|
|
1083
|
-
writeFileSync2(
|
|
1084
|
-
writeFileSync2(
|
|
1130
|
+
writeFileSync2(join3(output, "project.yml"), projectYml);
|
|
1131
|
+
writeFileSync2(join3(output, "Craft.entitlements"), renderEntitlements(config));
|
|
1132
|
+
writeFileSync2(join3(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
|
|
1085
1133
|
renderAssetCatalog(output, config);
|
|
1086
1134
|
if (config.enableLiveActivities) {
|
|
1087
|
-
mkdirSync2(
|
|
1088
|
-
const widgetInfo = readFileSync2(
|
|
1089
|
-
writeFileSync2(
|
|
1135
|
+
mkdirSync2(join3(output, "WidgetExtension"), { recursive: true });
|
|
1136
|
+
const widgetInfo = readFileSync2(join3(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
|
|
1137
|
+
writeFileSync2(join3(output, "WidgetExtension", "Info.plist"), widgetInfo);
|
|
1090
1138
|
}
|
|
1091
1139
|
if (config.enableWatchApp) {
|
|
1092
|
-
const watchInfo = readFileSync2(
|
|
1093
|
-
writeFileSync2(
|
|
1094
|
-
writeFileSync2(
|
|
1095
|
-
const watchExtensionInfo = readFileSync2(
|
|
1096
|
-
writeFileSync2(
|
|
1097
|
-
writeFileSync2(
|
|
1140
|
+
const watchInfo = readFileSync2(join3(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
1141
|
+
writeFileSync2(join3(output, "WatchApp", "Info.plist"), watchInfo);
|
|
1142
|
+
writeFileSync2(join3(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
1143
|
+
const watchExtensionInfo = readFileSync2(join3(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
1144
|
+
writeFileSync2(join3(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
|
|
1145
|
+
writeFileSync2(join3(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
1098
1146
|
}
|
|
1099
|
-
writeFileSync2(
|
|
1147
|
+
writeFileSync2(join3(output, "dist", "index.html"), renderOfflinePage(config));
|
|
1100
1148
|
if (usesNativeBundle(config)) {
|
|
1101
1149
|
if (config.nativeBundle)
|
|
1102
1150
|
installNativeBundle(config.nativeBundle, output);
|
|
@@ -1123,7 +1171,7 @@ async function build(options) {
|
|
|
1123
1171
|
const { htmlPath, nativeBundlePath, devServer, output, generateProject = true } = options;
|
|
1124
1172
|
console.log(`
|
|
1125
1173
|
\uD83D\uDCE6 Building Craft iOS project...`);
|
|
1126
|
-
const configPath =
|
|
1174
|
+
const configPath = join3(output, "craft.config.json");
|
|
1127
1175
|
if (!existsSync(configPath)) {
|
|
1128
1176
|
throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
|
|
1129
1177
|
}
|
|
@@ -1138,7 +1186,7 @@ async function build(options) {
|
|
|
1138
1186
|
const bundle = nativeBundlePath ?? config.nativeBundle;
|
|
1139
1187
|
if (bundle)
|
|
1140
1188
|
installNativeBundle(bundle, output);
|
|
1141
|
-
if (!existsSync(
|
|
1189
|
+
if (!existsSync(join3(output, "dist", "native-screen.js"))) {
|
|
1142
1190
|
throw new Error("Native iOS mode needs dist/native-screen.js. Compile a .stx screen with `stx native compile` first.");
|
|
1143
1191
|
}
|
|
1144
1192
|
} else if (nativeBundlePath && !isHybrid(config)) {
|
|
@@ -1164,7 +1212,7 @@ async function build(options) {
|
|
|
1164
1212
|
if (bundle) {
|
|
1165
1213
|
installNativeBundle(bundle, output);
|
|
1166
1214
|
console.log(` Native screens: ${bundle} \u2192 dist/native-screen.js`);
|
|
1167
|
-
} else if (!existsSync(
|
|
1215
|
+
} else if (!existsSync(join3(output, "dist", "native-screen.js"))) {
|
|
1168
1216
|
console.warn(" \u26A0 nativeScreens is set but there is no native bundle: every path opens in the web view.");
|
|
1169
1217
|
}
|
|
1170
1218
|
}
|
|
@@ -1189,12 +1237,12 @@ async function build(options) {
|
|
|
1189
1237
|
}
|
|
1190
1238
|
async function open(options) {
|
|
1191
1239
|
const { output } = options;
|
|
1192
|
-
const files =
|
|
1240
|
+
const files = readdirSync2(output);
|
|
1193
1241
|
const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
|
|
1194
1242
|
if (!xcodeproj) {
|
|
1195
1243
|
throw new Error(`No Xcode project found in ${output}. Run 'craft ios build' first.`);
|
|
1196
1244
|
}
|
|
1197
|
-
const projectPath =
|
|
1245
|
+
const projectPath = join3(output, xcodeproj);
|
|
1198
1246
|
console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
|
|
1199
1247
|
await $`open ${projectPath}`;
|
|
1200
1248
|
}
|
|
@@ -1235,7 +1283,7 @@ async function showSimulator() {
|
|
|
1235
1283
|
const selected = await $`xcode-select -p`.quiet().nothrow();
|
|
1236
1284
|
const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
|
|
1237
1285
|
if (developer) {
|
|
1238
|
-
const app =
|
|
1286
|
+
const app = join3(developer, "Applications", "Simulator.app");
|
|
1239
1287
|
if (existsSync(app)) {
|
|
1240
1288
|
await $`open ${app}`.quiet().nothrow();
|
|
1241
1289
|
return;
|
|
@@ -1246,14 +1294,14 @@ async function showSimulator() {
|
|
|
1246
1294
|
async function run(options) {
|
|
1247
1295
|
const { simulator, output } = options;
|
|
1248
1296
|
await build({ output });
|
|
1249
|
-
const files =
|
|
1297
|
+
const files = readdirSync2(output);
|
|
1250
1298
|
const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
|
|
1251
1299
|
if (!xcodeproj) {
|
|
1252
1300
|
throw new Error(`No Xcode project found in ${output}`);
|
|
1253
1301
|
}
|
|
1254
|
-
const projectPath =
|
|
1302
|
+
const projectPath = join3(output, xcodeproj);
|
|
1255
1303
|
const appName = xcodeproj.replace(".xcodeproj", "");
|
|
1256
|
-
const configPath =
|
|
1304
|
+
const configPath = join3(output, "craft.config.json");
|
|
1257
1305
|
if (!existsSync(configPath))
|
|
1258
1306
|
throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
|
|
1259
1307
|
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
@@ -1266,8 +1314,8 @@ async function run(options) {
|
|
|
1266
1314
|
console.log(` Device: ${device.name} (${device.runtime})`);
|
|
1267
1315
|
try {
|
|
1268
1316
|
await bootSimulator(device);
|
|
1269
|
-
const derivedData =
|
|
1270
|
-
const product =
|
|
1317
|
+
const derivedData = join3(output, "build");
|
|
1318
|
+
const product = join3(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
|
|
1271
1319
|
await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
|
|
1272
1320
|
if (!existsSync(product)) {
|
|
1273
1321
|
throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
|
|
@@ -1330,10 +1378,12 @@ export {
|
|
|
1330
1378
|
init,
|
|
1331
1379
|
hybridConfigProblems,
|
|
1332
1380
|
hasNativeRoutes,
|
|
1381
|
+
findApplicationSwiftSources,
|
|
1333
1382
|
createNativeMutationValidator,
|
|
1334
1383
|
build,
|
|
1335
1384
|
bootSimulator,
|
|
1336
1385
|
backgroundRefreshIdentifier,
|
|
1337
1386
|
associatedDomainEntries,
|
|
1387
|
+
assertNoApplicationSwiftSources,
|
|
1338
1388
|
LAUNCH_LOGO
|
|
1339
1389
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface SwiftSourceAuditOptions {
|
|
2
|
+
/** Paths, relative to `root`, that are generated or otherwise owned by Craft. */
|
|
3
|
+
ignoredPaths?: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
/** Return Swift files owned by an application rather than Craft's generated shell. */
|
|
6
|
+
export declare function findApplicationSwiftSources(root: string, options?: SwiftSourceAuditOptions): string[];
|
|
7
|
+
/** Fail a build when an application has started maintaining its own Swift source. */
|
|
8
|
+
export declare function assertNoApplicationSwiftSources(root: string, options?: SwiftSourceAuditOptions): void;
|