@craft-native/ios 0.0.173 → 0.0.174
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -1
- package/dist/cli.js +139 -79
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
- package/templates/CraftApp.swift +5 -0
- package/templates/CraftNativeActions.swift +174 -0
- package/templates/CraftNativeFlatList.swift +1 -1
- package/templates/CraftNativeScreen.swift +437 -38
package/README.md
CHANGED
|
@@ -120,7 +120,7 @@ ordinary native children while preserving the shared flex layout contract.
|
|
|
120
120
|
Keyboard frame changes update the native controller's additional safe area, so
|
|
121
121
|
focused inputs and responsive `onLayout` measurements move with the keyboard.
|
|
122
122
|
|
|
123
|
-
The opt-in `native` renderer hosts compiled `.stx` screens in JavaScriptCore and renders their `View`, `SafeAreaView`, `Text`, `Button`, `Link`, `Checkbox`, `TextInput`, `Picker`, `DatePicker`, `Switch`, `Slider`, `ActivityIndicator`, `Icon`, `Image`, `Video`, `ScrollView`, `SectionList`, and recycling `FlatList` nodes as UIKit controls. `Icon` uses SF Symbols with the shared symbol/name contract and accessibility metadata. `SectionList` uses the same native scroll host for ordinary `.stx` slot children; grouped section data should use `FlatList` until the section-data protocol is available. `FlatList` uses `UICollectionView` with stable keyed rows, incremental diffs, grids, horizontal and inverted layouts, list chrome, and end-reached pagination. Native views support `onLayout` callbacks with point-based frame measurements that refresh after safe-area or orientation changes. Flex items support `flex`, `flexGrow`, `flexShrink`, and numeric `flexBasis` for main-axis space distribution. Native screens also expose the versioned capability bridge: JSON storage and SQLite survive relaunch, lifecycle and deep-link events are scoped to the top route, and local notifications use `UNUserNotificationCenter`. Enable each capability in `craft.config.json`; disabled calls preserve Craft's `CAPABILITY_DISABLED` rejection code and every request has deterministic timeout/cancellation behavior. The default remains `web`; existing iOS apps are unchanged. `Checkbox` supports controlled `checked`/`value`, indeterminate state, and boolean `onValueChange`; `Picker` renders `<select>`/`<option>` values with `onValueChange` or `onChange`; `DatePicker` emits local date/time values plus ISO and millisecond timestamps; `Switch` supports state-aware `trackColor`, `thumbColor`, and iOS `ios_backgroundColor`; `Slider` supports track/thumb tint props and `step` snapping; `Image` reports load lifecycle events and structured failures; `Video` uses an `AVPlayerViewController` attached to the native screen for URI playback, native controls, and route-safe teardown. The portable component, capability, list, style, image-source, event, accessibility, and reconciliation behavior is specified in the [Native STX guide](../../docs/guides/native-stx.md).
|
|
123
|
+
The opt-in `native` renderer hosts compiled `.stx` screens in JavaScriptCore and renders their `View`, `SafeAreaView`, `Text`, `Button`, `Link`, `Checkbox`, `TextInput`, `Picker`, `DatePicker`, `Switch`, `Slider`, `ActivityIndicator`, `Icon`, `Image`, `Video`, `Map`, `ScrollView`, `SectionList`, and recycling `FlatList` nodes as UIKit controls. `Map` uses MapKit for trail polylines and marker taps; its overlays carry flat latitude/longitude `geometry` arrays and can fit the visible region to their bounds. `Icon` uses SF Symbols with the shared symbol/name contract and accessibility metadata. `SectionList` uses the same native scroll host for ordinary `.stx` slot children; grouped section data should use `FlatList` until the section-data protocol is available. `FlatList` uses `UICollectionView` with stable keyed rows, incremental diffs, grids, horizontal and inverted layouts, list chrome, and end-reached pagination. Native views support `onLayout` callbacks with point-based frame measurements that refresh after safe-area or orientation changes. Flex items support `flex`, `flexGrow`, `flexShrink`, and numeric `flexBasis` for main-axis space distribution. Native screens also expose the versioned capability bridge: JSON storage and SQLite survive relaunch, lifecycle and deep-link events are scoped to the top route, and local notifications use `UNUserNotificationCenter`. Enable each capability in `craft.config.json`; disabled calls preserve Craft's `CAPABILITY_DISABLED` rejection code and every request has deterministic timeout/cancellation behavior. The default remains `web`; existing iOS apps are unchanged. `Checkbox` supports controlled `checked`/`value`, indeterminate state, and boolean `onValueChange`; `Picker` renders `<select>`/`<option>` values with `onValueChange` or `onChange`; `DatePicker` emits local date/time values plus ISO and millisecond timestamps; `Switch` supports state-aware `trackColor`, `thumbColor`, and iOS `ios_backgroundColor`; `Slider` supports track/thumb tint props and `step` snapping; `Image` reports load lifecycle events and structured failures; `Video` uses an `AVPlayerViewController` attached to the native screen for URI playback, native controls, and route-safe teardown. The portable component, capability, list, style, image-source, event, accessibility, and reconciliation behavior is specified in the [Native STX guide](../../docs/guides/native-stx.md).
|
|
124
124
|
|
|
125
125
|
```bash
|
|
126
126
|
craft ios init NativeDemo --renderer native --output ./ios-native
|
|
@@ -131,6 +131,17 @@ craft ios run --output ./ios-native --simulator
|
|
|
131
131
|
|
|
132
132
|
`stx native compile` is part of the [stx](https://github.com/stacksjs/stx) CLI (`@stacksjs/stx`); build tools can call `compileNativeBundle` from `@stacksjs/stx/native` instead. A runnable screen is in [`fixtures/native-screen/Screen.stx`](./fixtures/native-screen/Screen.stx). In native mode, the app never instantiates `WKWebView`; a missing `dist/native-screen.js` fails the build rather than silently opening the web placeholder.
|
|
133
133
|
|
|
134
|
+
Keep application repositories free of handwritten Swift with the built-in audit:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
craft-ios audit-sources --app-root .
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The command ignores Craft's generated `storage/framework/mobile` tree, build
|
|
141
|
+
output, and dependencies. It exits non-zero and lists the relative paths when
|
|
142
|
+
an app owns a `.swift` file; use the exported `assertNoApplicationSwiftSources`
|
|
143
|
+
function when the check belongs inside an existing TypeScript test.
|
|
144
|
+
|
|
134
145
|
Native screen updates use the versioned mutation protocol when the bundle
|
|
135
146
|
supports it and fall back to whole-document rendering for older bundles.
|
|
136
147
|
Updates reconcile the UIKit tree by sibling-scoped `key` (or `testID` for
|
|
@@ -163,6 +174,23 @@ The optional Liquid Glass tab-bar styling is compiler-gated for Xcode 26/Swift 6
|
|
|
163
174
|
|
|
164
175
|
The iOS host overlays `craft.config.json` on its defaults. Missing or `null` capability flags stay disabled; explicitly set a flag to `true` to enable it. Both the Swift host and Zig bridge reject malformed values and fall back to all capabilities disabled.
|
|
165
176
|
|
|
177
|
+
Native passkeys require both `enablePasskeys: true` and a matching
|
|
178
|
+
`webcredentials:<host>` entry in `associatedDomains`:
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"renderer": "native",
|
|
183
|
+
"enablePasskeys": true,
|
|
184
|
+
"associatedDomains": ["webcredentials:login.example.com"]
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The native STX screen calls `craft.passkeys.isAvailable()`, `create(options)`,
|
|
189
|
+
or `get(options)`. The host validates the relying-party id against those
|
|
190
|
+
associated domains before showing Apple's `ASAuthorization` sheet; a passkey
|
|
191
|
+
request for another host is rejected. The server remains responsible for
|
|
192
|
+
generating the challenge and verifying the returned WebAuthn response.
|
|
193
|
+
|
|
166
194
|
Edit `craft.config.json` in your iOS project:
|
|
167
195
|
|
|
168
196
|
```json
|
|
@@ -189,6 +217,8 @@ Edit `craft.config.json` in your iOS project:
|
|
|
189
217
|
"enableFilePicker": true,
|
|
190
218
|
"enableFileDownload": true,
|
|
191
219
|
"enableSocialAuth": true,
|
|
220
|
+
"enablePasskeys": true,
|
|
221
|
+
"associatedDomains": ["webcredentials:login.example.com"],
|
|
192
222
|
"enableAudioRecording": true,
|
|
193
223
|
"enableVideoRecording": true,
|
|
194
224
|
"enableMotionSensors": true,
|
package/dist/cli.js
CHANGED
|
@@ -3,23 +3,72 @@
|
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
5
|
import { parseArgs } from "util";
|
|
6
|
+
import { resolve as resolve2 } from "path";
|
|
6
7
|
|
|
7
8
|
// src/index.ts
|
|
8
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
|
|
9
|
-
import { dirname as dirname2, join as
|
|
9
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync as readdirSync2, rmSync, statSync, writeFileSync } from "fs";
|
|
10
|
+
import { dirname as dirname2, join as join3, resolve } from "path";
|
|
10
11
|
var {$ } = globalThis.Bun;
|
|
11
12
|
|
|
13
|
+
// src/source-audit.ts
|
|
14
|
+
import { readdirSync } from "fs";
|
|
15
|
+
import { join, relative, sep } from "path";
|
|
16
|
+
var defaultIgnoredPaths = [
|
|
17
|
+
".git",
|
|
18
|
+
"node_modules",
|
|
19
|
+
"dist",
|
|
20
|
+
"build",
|
|
21
|
+
"storage/framework/mobile"
|
|
22
|
+
];
|
|
23
|
+
function normalisePath(path) {
|
|
24
|
+
return path.split(sep).join("/");
|
|
25
|
+
}
|
|
26
|
+
function isIgnored(path, ignoredPaths) {
|
|
27
|
+
const normalised = normalisePath(path);
|
|
28
|
+
return ignoredPaths.some((ignored) => {
|
|
29
|
+
const prefix = normalisePath(ignored).replace(/\/+$/, "");
|
|
30
|
+
return normalised === prefix || normalised.startsWith(`${prefix}/`);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function visit(root, current, ignoredPaths, found) {
|
|
34
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
35
|
+
const absolute = join(current, entry.name);
|
|
36
|
+
const relativePath = relative(root, absolute);
|
|
37
|
+
if (isIgnored(relativePath, ignoredPaths))
|
|
38
|
+
continue;
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
visit(root, absolute, ignoredPaths, found);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (entry.isFile() && entry.name.endsWith(".swift"))
|
|
44
|
+
found.push(normalisePath(relativePath));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function findApplicationSwiftSources(root, options = {}) {
|
|
48
|
+
const ignoredPaths = [...defaultIgnoredPaths, ...options.ignoredPaths ?? []];
|
|
49
|
+
const found = [];
|
|
50
|
+
visit(root, root, ignoredPaths, found);
|
|
51
|
+
return found.sort();
|
|
52
|
+
}
|
|
53
|
+
function assertNoApplicationSwiftSources(root, options = {}) {
|
|
54
|
+
const sources = findApplicationSwiftSources(root, options);
|
|
55
|
+
if (sources.length > 0) {
|
|
56
|
+
throw new Error(`Application-owned Swift sources found:
|
|
57
|
+
${sources.map((path) => `- ${path}`).join(`
|
|
58
|
+
`)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
12
61
|
// src/testing.ts
|
|
13
|
-
import { dirname, join } from "path";
|
|
14
|
-
var templates =
|
|
62
|
+
import { dirname, join as join2 } from "path";
|
|
63
|
+
var templates = join2(dirname(import.meta.dir), "templates/testing");
|
|
15
64
|
|
|
16
65
|
// src/index.ts
|
|
17
|
-
var TEMPLATES_DIR =
|
|
66
|
+
var TEMPLATES_DIR = join3(dirname2(import.meta.dir), "templates");
|
|
18
67
|
function resolveCraftVersion(startDir = import.meta.dir) {
|
|
19
68
|
let directory = resolve(startDir);
|
|
20
69
|
while (true) {
|
|
21
70
|
try {
|
|
22
|
-
const packageJSON = JSON.parse(readFileSync(
|
|
71
|
+
const packageJSON = JSON.parse(readFileSync(join3(directory, "package.json"), "utf8"));
|
|
23
72
|
if (packageJSON.name === "@craft-native/ios" && typeof packageJSON.version === "string" && packageJSON.version.length > 0)
|
|
24
73
|
return packageJSON.version;
|
|
25
74
|
} catch {}
|
|
@@ -171,7 +220,7 @@ function hybridConfigProblems(config) {
|
|
|
171
220
|
function installNativeBundle(source, output) {
|
|
172
221
|
if (!existsSync(source))
|
|
173
222
|
throw new Error(`Native bundle not found: ${source}`);
|
|
174
|
-
const destination =
|
|
223
|
+
const destination = join3(output, "dist", "native-screen.js");
|
|
175
224
|
mkdirSync(dirname2(destination), { recursive: true });
|
|
176
225
|
if (resolve(source) !== resolve(destination))
|
|
177
226
|
cpSync(source, destination);
|
|
@@ -203,6 +252,7 @@ var DEFAULT_CONFIG = {
|
|
|
203
252
|
enableFilePicker: false,
|
|
204
253
|
enableFileDownload: false,
|
|
205
254
|
enableSocialAuth: false,
|
|
255
|
+
enablePasskeys: false,
|
|
206
256
|
enableAudioRecording: false,
|
|
207
257
|
enableVideoRecording: false,
|
|
208
258
|
enableMotionSensors: false,
|
|
@@ -547,20 +597,20 @@ function writeOpaqueIcon(source, target) {
|
|
|
547
597
|
}
|
|
548
598
|
}
|
|
549
599
|
function renderAssetCatalog(output, config) {
|
|
550
|
-
const catalog =
|
|
551
|
-
const appIcon =
|
|
552
|
-
const launchBackground =
|
|
600
|
+
const catalog = join3(output, "Assets.xcassets");
|
|
601
|
+
const appIcon = join3(catalog, "AppIcon.appiconset");
|
|
602
|
+
const launchBackground = join3(catalog, "LaunchBackground.colorset");
|
|
553
603
|
mkdirSync(appIcon, { recursive: true });
|
|
554
604
|
mkdirSync(launchBackground, { recursive: true });
|
|
555
|
-
writeFileSync(
|
|
605
|
+
writeFileSync(join3(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
|
|
556
606
|
`);
|
|
557
607
|
const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
|
|
558
608
|
if (config.appIconPath) {
|
|
559
609
|
if (!existsSync(config.appIconPath))
|
|
560
610
|
throw new Error(`App icon not found: ${config.appIconPath}`);
|
|
561
|
-
writeOpaqueIcon(config.appIconPath,
|
|
611
|
+
writeOpaqueIcon(config.appIconPath, join3(appIcon, iconFilename));
|
|
562
612
|
}
|
|
563
|
-
writeFileSync(
|
|
613
|
+
writeFileSync(join3(appIcon, "Contents.json"), `${JSON.stringify({
|
|
564
614
|
images: iconFilename ? [{
|
|
565
615
|
filename: iconFilename,
|
|
566
616
|
idiom: "universal",
|
|
@@ -587,7 +637,7 @@ function renderAssetCatalog(output, config) {
|
|
|
587
637
|
idiom: "universal"
|
|
588
638
|
});
|
|
589
639
|
}
|
|
590
|
-
writeFileSync(
|
|
640
|
+
writeFileSync(join3(launchBackground, "Contents.json"), `${JSON.stringify({
|
|
591
641
|
colors,
|
|
592
642
|
info: { author: "xcode", version: 1 }
|
|
593
643
|
}, null, 2)}
|
|
@@ -596,7 +646,7 @@ function renderAssetCatalog(output, config) {
|
|
|
596
646
|
}
|
|
597
647
|
var LAUNCH_LOGO = "LaunchLogo";
|
|
598
648
|
function renderLaunchLogo(catalog, config) {
|
|
599
|
-
const imageset =
|
|
649
|
+
const imageset = join3(catalog, `${LAUNCH_LOGO}.imageset`);
|
|
600
650
|
rmSync(imageset, { recursive: true, force: true });
|
|
601
651
|
if (!config.splashImagePath)
|
|
602
652
|
return;
|
|
@@ -612,14 +662,14 @@ function renderLaunchLogo(catalog, config) {
|
|
|
612
662
|
if (extension === "svg" || extension === "pdf")
|
|
613
663
|
vector = true;
|
|
614
664
|
const filename = `${LAUNCH_LOGO}${dark ? "-dark" : ""}.${extension}`;
|
|
615
|
-
cpSync(source,
|
|
665
|
+
cpSync(source, join3(imageset, filename));
|
|
616
666
|
images.push({
|
|
617
667
|
...dark ? { appearances: [{ appearance: "luminosity", value: "dark" }] } : {},
|
|
618
668
|
filename,
|
|
619
669
|
idiom: "universal"
|
|
620
670
|
});
|
|
621
671
|
}
|
|
622
|
-
writeFileSync(
|
|
672
|
+
writeFileSync(join3(imageset, "Contents.json"), `${JSON.stringify({
|
|
623
673
|
images,
|
|
624
674
|
info: { author: "xcode", version: 1 },
|
|
625
675
|
...vector ? { properties: { "preserves-vector-representation": true } } : {}
|
|
@@ -733,15 +783,15 @@ function syncWebAssets(source, output) {
|
|
|
733
783
|
const sourcePath = resolve(source);
|
|
734
784
|
if (!existsSync(sourcePath))
|
|
735
785
|
throw new Error(`Web asset path not found: ${source}`);
|
|
736
|
-
const distDir =
|
|
786
|
+
const distDir = join3(output, "dist");
|
|
737
787
|
rmSync(distDir, { recursive: true, force: true });
|
|
738
788
|
mkdirSync(distDir, { recursive: true });
|
|
739
789
|
if (statSync(sourcePath).isDirectory()) {
|
|
740
790
|
cpSync(sourcePath, distDir, { recursive: true });
|
|
741
791
|
} else {
|
|
742
|
-
cpSync(sourcePath,
|
|
792
|
+
cpSync(sourcePath, join3(distDir, "index.html"));
|
|
743
793
|
}
|
|
744
|
-
if (!existsSync(
|
|
794
|
+
if (!existsSync(join3(distDir, "index.html"))) {
|
|
745
795
|
throw new Error(`Web asset directory must contain index.html: ${source}`);
|
|
746
796
|
}
|
|
747
797
|
}
|
|
@@ -763,32 +813,32 @@ var RUNTIME_ARCHIVES = {
|
|
|
763
813
|
};
|
|
764
814
|
async function installRuntime(output, runtimeDir) {
|
|
765
815
|
const resolved = Object.entries(RUNTIME_ARCHIVES).map(([sdk, archives]) => {
|
|
766
|
-
const present = archives.filter((a) => existsSync(
|
|
816
|
+
const present = archives.filter((a) => existsSync(join3(runtimeDir, a)));
|
|
767
817
|
if (present.length === 0) {
|
|
768
818
|
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.`);
|
|
769
819
|
}
|
|
770
820
|
return { sdk, archives, present };
|
|
771
821
|
});
|
|
772
|
-
const dest =
|
|
822
|
+
const dest = join3(output, "Runtime");
|
|
773
823
|
rmSync(dest, { recursive: true, force: true });
|
|
774
824
|
for (const { sdk, archives, present } of resolved) {
|
|
775
|
-
const sdkDir =
|
|
825
|
+
const sdkDir = join3(dest, sdk);
|
|
776
826
|
mkdirSync(sdkDir, { recursive: true });
|
|
777
|
-
const target =
|
|
827
|
+
const target = join3(sdkDir, "libcraft-ios.a");
|
|
778
828
|
if (present.length === 1) {
|
|
779
829
|
const missing = archives.filter((a) => !present.includes(a));
|
|
780
830
|
if (missing.length > 0) {
|
|
781
831
|
console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
|
|
782
832
|
}
|
|
783
|
-
cpSync(
|
|
833
|
+
cpSync(join3(runtimeDir, present[0]), target);
|
|
784
834
|
} else {
|
|
785
|
-
await $`lipo -create ${present.map((a) =>
|
|
835
|
+
await $`lipo -create ${present.map((a) => join3(runtimeDir, a))} -output ${target}`.quiet();
|
|
786
836
|
}
|
|
787
837
|
}
|
|
788
838
|
return true;
|
|
789
839
|
}
|
|
790
840
|
async function refreshRuntime(output, override) {
|
|
791
|
-
if (!existsSync(
|
|
841
|
+
if (!existsSync(join3(output, "Runtime")))
|
|
792
842
|
return;
|
|
793
843
|
const dir = resolveRuntimeDir(override);
|
|
794
844
|
if (!dir) {
|
|
@@ -809,34 +859,34 @@ function writeShell(output, config) {
|
|
|
809
859
|
if (!config.appName || !/^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(config.bundleId))
|
|
810
860
|
throw new Error("The iOS shell needs an appName and a valid bundleId in craft.config.json.");
|
|
811
861
|
const name = productName(config.appName);
|
|
812
|
-
mkdirSync(
|
|
813
|
-
mkdirSync(
|
|
814
|
-
const swiftTemplate = readFileSync(
|
|
862
|
+
mkdirSync(join3(output, "Sources"), { recursive: true });
|
|
863
|
+
mkdirSync(join3(output, "Shared"), { recursive: true });
|
|
864
|
+
const swiftTemplate = readFileSync(join3(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
|
|
815
865
|
const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, config.bundleId).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION);
|
|
816
|
-
writeFileSync(
|
|
817
|
-
cpSync(
|
|
818
|
-
cpSync(
|
|
819
|
-
cpSync(
|
|
820
|
-
cpSync(
|
|
821
|
-
cpSync(
|
|
822
|
-
cpSync(
|
|
823
|
-
cpSync(
|
|
824
|
-
cpSync(
|
|
825
|
-
cpSync(
|
|
826
|
-
cpSync(
|
|
827
|
-
cpSync(
|
|
828
|
-
cpSync(
|
|
829
|
-
cpSync(
|
|
830
|
-
cpSync(
|
|
866
|
+
writeFileSync(join3(output, "Sources", `${name}App.swift`), swiftSource);
|
|
867
|
+
cpSync(join3(TEMPLATES_DIR, "CraftNativeScreen.swift"), join3(output, "Sources", "CraftNativeScreen.swift"));
|
|
868
|
+
cpSync(join3(TEMPLATES_DIR, "CraftNativeFlatList.swift"), join3(output, "Sources", "CraftNativeFlatList.swift"));
|
|
869
|
+
cpSync(join3(TEMPLATES_DIR, "CraftNativeActions.swift"), join3(output, "Sources", "CraftNativeActions.swift"));
|
|
870
|
+
cpSync(join3(TEMPLATES_DIR, "CraftHealth.swift"), join3(output, "Sources", "CraftHealth.swift"));
|
|
871
|
+
cpSync(join3(TEMPLATES_DIR, "CraftMusic.swift"), join3(output, "Sources", "CraftMusic.swift"));
|
|
872
|
+
cpSync(join3(TEMPLATES_DIR, "CraftHeartRate.swift"), join3(output, "Sources", "CraftHeartRate.swift"));
|
|
873
|
+
cpSync(join3(TEMPLATES_DIR, "CraftScheduledWorkouts.swift"), join3(output, "Sources", "CraftScheduledWorkouts.swift"));
|
|
874
|
+
cpSync(join3(TEMPLATES_DIR, "CraftNativeMutation.swift"), join3(output, "Sources", "CraftNativeMutation.swift"));
|
|
875
|
+
cpSync(join3(TEMPLATES_DIR, "CraftNativeUI.swift"), join3(output, "Sources", "CraftNativeUI.swift"));
|
|
876
|
+
cpSync(join3(TEMPLATES_DIR, "CraftNativeServices.swift"), join3(output, "Sources", "CraftNativeServices.swift"));
|
|
877
|
+
cpSync(join3(TEMPLATES_DIR, "CraftNativeFiles.swift"), join3(output, "Sources", "CraftNativeFiles.swift"));
|
|
878
|
+
cpSync(join3(TEMPLATES_DIR, "CraftWebAudio.swift"), join3(output, "Sources", "CraftWebAudio.swift"));
|
|
879
|
+
cpSync(join3(TEMPLATES_DIR, "CraftHybrid.swift"), join3(output, "Sources", "CraftHybrid.swift"));
|
|
880
|
+
cpSync(join3(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join3(output, "Shared", "CraftActivityAttributes.swift"));
|
|
831
881
|
for (const [enabled, folder, template, filename] of [
|
|
832
882
|
[config.enableLiveActivities, "WidgetExtension", "CraftLiveActivityWidget.swift.template", `${name}LiveActivity.swift`],
|
|
833
883
|
[config.enableWatchApp, "WatchExtension", "CraftWatchApp.swift.template", `${name}WatchApp.swift`]
|
|
834
884
|
]) {
|
|
835
885
|
if (!enabled)
|
|
836
886
|
continue;
|
|
837
|
-
mkdirSync(
|
|
838
|
-
const source = readFileSync(
|
|
839
|
-
writeFileSync(
|
|
887
|
+
mkdirSync(join3(output, folder), { recursive: true });
|
|
888
|
+
const source = readFileSync(join3(TEMPLATES_DIR, template), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
|
|
889
|
+
writeFileSync(join3(output, folder, filename), source);
|
|
840
890
|
}
|
|
841
891
|
}
|
|
842
892
|
async function init(options) {
|
|
@@ -846,9 +896,9 @@ async function init(options) {
|
|
|
846
896
|
\u26A1 Initializing Craft iOS project: ${displayName}`);
|
|
847
897
|
console.log(` Output: ${output}
|
|
848
898
|
`);
|
|
849
|
-
const dirs = [output,
|
|
899
|
+
const dirs = [output, join3(output, "Sources"), join3(output, "Shared"), join3(output, "dist")];
|
|
850
900
|
if (options.config?.enableWatchApp)
|
|
851
|
-
dirs.push(
|
|
901
|
+
dirs.push(join3(output, "WatchApp"), join3(output, "WatchExtension"));
|
|
852
902
|
for (const dir of dirs) {
|
|
853
903
|
if (!existsSync(dir)) {
|
|
854
904
|
mkdirSync(dir, { recursive: true });
|
|
@@ -875,15 +925,15 @@ async function init(options) {
|
|
|
875
925
|
`));
|
|
876
926
|
if (Object.keys(config.shareStorage ?? {}).length)
|
|
877
927
|
config.enableSecureStorage = true;
|
|
878
|
-
writeFileSync(
|
|
928
|
+
writeFileSync(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
879
929
|
writeShell(output, config);
|
|
880
|
-
const infoPlistTemplate = readFileSync(
|
|
930
|
+
const infoPlistTemplate = readFileSync(join3(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
|
|
881
931
|
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>
|
|
882
932
|
<true/>
|
|
883
933
|
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
|
|
884
934
|
<true/>` : "");
|
|
885
|
-
writeFileSync(
|
|
886
|
-
const projectYmlTemplate = readFileSync(
|
|
935
|
+
writeFileSync(join3(output, "Info.plist"), infoPlist);
|
|
936
|
+
const projectYmlTemplate = readFileSync(join3(TEMPLATES_DIR, "project.yml.template"), "utf-8");
|
|
887
937
|
const nativeDependencies = [];
|
|
888
938
|
if (config.enableLiveActivities)
|
|
889
939
|
nativeDependencies.push(` - target: ${name}LiveActivity`);
|
|
@@ -940,30 +990,30 @@ async function init(options) {
|
|
|
940
990
|
if (hasRuntime) {
|
|
941
991
|
console.log(" Linked the Zig runtime from", runtimeDir);
|
|
942
992
|
} else {
|
|
943
|
-
rmSync(
|
|
993
|
+
rmSync(join3(output, "Runtime"), { recursive: true, force: true });
|
|
944
994
|
}
|
|
945
995
|
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:
|
|
946
996
|
${nativeDependencies.join(`
|
|
947
997
|
`)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
|
|
948
998
|
`));
|
|
949
|
-
writeFileSync(
|
|
950
|
-
writeFileSync(
|
|
951
|
-
writeFileSync(
|
|
999
|
+
writeFileSync(join3(output, "project.yml"), projectYml);
|
|
1000
|
+
writeFileSync(join3(output, "Craft.entitlements"), renderEntitlements(config));
|
|
1001
|
+
writeFileSync(join3(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
|
|
952
1002
|
renderAssetCatalog(output, config);
|
|
953
1003
|
if (config.enableLiveActivities) {
|
|
954
|
-
mkdirSync(
|
|
955
|
-
const widgetInfo = readFileSync(
|
|
956
|
-
writeFileSync(
|
|
1004
|
+
mkdirSync(join3(output, "WidgetExtension"), { recursive: true });
|
|
1005
|
+
const widgetInfo = readFileSync(join3(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
|
|
1006
|
+
writeFileSync(join3(output, "WidgetExtension", "Info.plist"), widgetInfo);
|
|
957
1007
|
}
|
|
958
1008
|
if (config.enableWatchApp) {
|
|
959
|
-
const watchInfo = readFileSync(
|
|
960
|
-
writeFileSync(
|
|
961
|
-
writeFileSync(
|
|
962
|
-
const watchExtensionInfo = readFileSync(
|
|
963
|
-
writeFileSync(
|
|
964
|
-
writeFileSync(
|
|
1009
|
+
const watchInfo = readFileSync(join3(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
1010
|
+
writeFileSync(join3(output, "WatchApp", "Info.plist"), watchInfo);
|
|
1011
|
+
writeFileSync(join3(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
1012
|
+
const watchExtensionInfo = readFileSync(join3(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
1013
|
+
writeFileSync(join3(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
|
|
1014
|
+
writeFileSync(join3(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
965
1015
|
}
|
|
966
|
-
writeFileSync(
|
|
1016
|
+
writeFileSync(join3(output, "dist", "index.html"), renderOfflinePage(config));
|
|
967
1017
|
if (usesNativeBundle(config)) {
|
|
968
1018
|
if (config.nativeBundle)
|
|
969
1019
|
installNativeBundle(config.nativeBundle, output);
|
|
@@ -990,7 +1040,7 @@ async function build(options) {
|
|
|
990
1040
|
const { htmlPath, nativeBundlePath, devServer, output, generateProject = true } = options;
|
|
991
1041
|
console.log(`
|
|
992
1042
|
\uD83D\uDCE6 Building Craft iOS project...`);
|
|
993
|
-
const configPath =
|
|
1043
|
+
const configPath = join3(output, "craft.config.json");
|
|
994
1044
|
if (!existsSync(configPath)) {
|
|
995
1045
|
throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
|
|
996
1046
|
}
|
|
@@ -1005,7 +1055,7 @@ async function build(options) {
|
|
|
1005
1055
|
const bundle = nativeBundlePath ?? config.nativeBundle;
|
|
1006
1056
|
if (bundle)
|
|
1007
1057
|
installNativeBundle(bundle, output);
|
|
1008
|
-
if (!existsSync(
|
|
1058
|
+
if (!existsSync(join3(output, "dist", "native-screen.js"))) {
|
|
1009
1059
|
throw new Error("Native iOS mode needs dist/native-screen.js. Compile a .stx screen with `stx native compile` first.");
|
|
1010
1060
|
}
|
|
1011
1061
|
} else if (nativeBundlePath && !isHybrid(config)) {
|
|
@@ -1031,7 +1081,7 @@ async function build(options) {
|
|
|
1031
1081
|
if (bundle) {
|
|
1032
1082
|
installNativeBundle(bundle, output);
|
|
1033
1083
|
console.log(` Native screens: ${bundle} \u2192 dist/native-screen.js`);
|
|
1034
|
-
} else if (!existsSync(
|
|
1084
|
+
} else if (!existsSync(join3(output, "dist", "native-screen.js"))) {
|
|
1035
1085
|
console.warn(" \u26A0 nativeScreens is set but there is no native bundle: every path opens in the web view.");
|
|
1036
1086
|
}
|
|
1037
1087
|
}
|
|
@@ -1056,12 +1106,12 @@ async function build(options) {
|
|
|
1056
1106
|
}
|
|
1057
1107
|
async function open(options) {
|
|
1058
1108
|
const { output } = options;
|
|
1059
|
-
const files =
|
|
1109
|
+
const files = readdirSync2(output);
|
|
1060
1110
|
const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
|
|
1061
1111
|
if (!xcodeproj) {
|
|
1062
1112
|
throw new Error(`No Xcode project found in ${output}. Run 'craft ios build' first.`);
|
|
1063
1113
|
}
|
|
1064
|
-
const projectPath =
|
|
1114
|
+
const projectPath = join3(output, xcodeproj);
|
|
1065
1115
|
console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
|
|
1066
1116
|
await $`open ${projectPath}`;
|
|
1067
1117
|
}
|
|
@@ -1102,7 +1152,7 @@ async function showSimulator() {
|
|
|
1102
1152
|
const selected = await $`xcode-select -p`.quiet().nothrow();
|
|
1103
1153
|
const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
|
|
1104
1154
|
if (developer) {
|
|
1105
|
-
const app =
|
|
1155
|
+
const app = join3(developer, "Applications", "Simulator.app");
|
|
1106
1156
|
if (existsSync(app)) {
|
|
1107
1157
|
await $`open ${app}`.quiet().nothrow();
|
|
1108
1158
|
return;
|
|
@@ -1113,14 +1163,14 @@ async function showSimulator() {
|
|
|
1113
1163
|
async function run(options) {
|
|
1114
1164
|
const { simulator, output } = options;
|
|
1115
1165
|
await build({ output });
|
|
1116
|
-
const files =
|
|
1166
|
+
const files = readdirSync2(output);
|
|
1117
1167
|
const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
|
|
1118
1168
|
if (!xcodeproj) {
|
|
1119
1169
|
throw new Error(`No Xcode project found in ${output}`);
|
|
1120
1170
|
}
|
|
1121
|
-
const projectPath =
|
|
1171
|
+
const projectPath = join3(output, xcodeproj);
|
|
1122
1172
|
const appName = xcodeproj.replace(".xcodeproj", "");
|
|
1123
|
-
const configPath =
|
|
1173
|
+
const configPath = join3(output, "craft.config.json");
|
|
1124
1174
|
if (!existsSync(configPath))
|
|
1125
1175
|
throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
|
|
1126
1176
|
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
@@ -1133,8 +1183,8 @@ async function run(options) {
|
|
|
1133
1183
|
console.log(` Device: ${device.name} (${device.runtime})`);
|
|
1134
1184
|
try {
|
|
1135
1185
|
await bootSimulator(device);
|
|
1136
|
-
const derivedData =
|
|
1137
|
-
const product =
|
|
1186
|
+
const derivedData = join3(output, "build");
|
|
1187
|
+
const product = join3(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
|
|
1138
1188
|
await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
|
|
1139
1189
|
if (!existsSync(product)) {
|
|
1140
1190
|
throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
|
|
@@ -1171,6 +1221,7 @@ var { values, positionals } = parseArgs({
|
|
|
1171
1221
|
"html-path": { type: "string" },
|
|
1172
1222
|
"native-bundle": { type: "string" },
|
|
1173
1223
|
"dev-server": { type: "string", short: "d" },
|
|
1224
|
+
"app-root": { type: "string" },
|
|
1174
1225
|
output: { type: "string", short: "o" },
|
|
1175
1226
|
simulator: { type: "boolean", short: "s" }
|
|
1176
1227
|
},
|
|
@@ -1187,6 +1238,7 @@ Usage:
|
|
|
1187
1238
|
Commands:
|
|
1188
1239
|
init <name> Initialize a new iOS project
|
|
1189
1240
|
build Build web assets and generate Xcode project
|
|
1241
|
+
audit-sources Fail when the app owns Swift outside Craft's generated output
|
|
1190
1242
|
open Open Xcode project
|
|
1191
1243
|
run Build and run on simulator
|
|
1192
1244
|
|
|
@@ -1198,6 +1250,7 @@ Options:
|
|
|
1198
1250
|
--html-path <path> Path to HTML file or directory
|
|
1199
1251
|
--native-bundle <path> JavaScript bundle from \`stx native compile\`
|
|
1200
1252
|
-d, --dev-server <url> Development server URL
|
|
1253
|
+
--app-root <dir> Application root for audit-sources (default: .)
|
|
1201
1254
|
-o, --output <dir> Output directory (default: ./ios)
|
|
1202
1255
|
-s, --simulator Run on simulator instead of device
|
|
1203
1256
|
-h, --help Show help
|
|
@@ -1208,6 +1261,7 @@ Examples:
|
|
|
1208
1261
|
craft-ios build --html-path ./dist/index.html
|
|
1209
1262
|
craft-ios init MyApp --renderer native
|
|
1210
1263
|
craft-ios build --native-bundle ./screen.js
|
|
1264
|
+
craft-ios audit-sources --app-root .
|
|
1211
1265
|
craft-ios build --dev-server http://localhost:3456
|
|
1212
1266
|
craft-ios run --simulator
|
|
1213
1267
|
`);
|
|
@@ -1237,6 +1291,12 @@ async function main() {
|
|
|
1237
1291
|
output: values.output || "./ios"
|
|
1238
1292
|
});
|
|
1239
1293
|
break;
|
|
1294
|
+
case "audit-sources": {
|
|
1295
|
+
const root = resolve2(values["app-root"] || ".");
|
|
1296
|
+
assertNoApplicationSwiftSources(root);
|
|
1297
|
+
console.log(`\u2705 No application-owned Swift sources found in ${root}`);
|
|
1298
|
+
break;
|
|
1299
|
+
}
|
|
1240
1300
|
case "open":
|
|
1241
1301
|
await open({
|
|
1242
1302
|
output: values.output || "./ios"
|
package/dist/index.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface CraftConfig {
|
|
|
47
47
|
enableFilePicker?: boolean;
|
|
48
48
|
enableFileDownload?: boolean;
|
|
49
49
|
enableSocialAuth?: boolean;
|
|
50
|
+
/** Native passkeys; requires a matching `webcredentials:` associated domain. */
|
|
51
|
+
enablePasskeys?: boolean;
|
|
50
52
|
enableAudioRecording?: boolean;
|
|
51
53
|
enableVideoRecording?: boolean;
|
|
52
54
|
enableMotionSensors?: boolean;
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
package/templates/CraftApp.swift
CHANGED
|
@@ -1239,6 +1239,9 @@ struct CraftConfig: Codable {
|
|
|
1239
1239
|
var enableFilePicker: Bool = false
|
|
1240
1240
|
var enableFileDownload: Bool = false
|
|
1241
1241
|
var enableSocialAuth: Bool = false
|
|
1242
|
+
/// Native passkeys through AuthenticationServices. Requires a matching
|
|
1243
|
+
/// `webcredentials:` associated-domain entry.
|
|
1244
|
+
var enablePasskeys: Bool = false
|
|
1242
1245
|
var enableAudioRecording: Bool = false
|
|
1243
1246
|
var enableVideoRecording: Bool = false
|
|
1244
1247
|
var enableMotionSensors: Bool = false
|
|
@@ -1264,6 +1267,8 @@ struct CraftConfig: Codable {
|
|
|
1264
1267
|
var enableScheduledWorkouts: Bool = false
|
|
1265
1268
|
var devServerURL: String? = nil
|
|
1266
1269
|
var trustedOrigins: [String] = []
|
|
1270
|
+
/// Domains allowed to back native passkey relying-party identifiers.
|
|
1271
|
+
var associatedDomains: [String] = []
|
|
1267
1272
|
/// The longest the launch splash stays up when the page neither says it is
|
|
1268
1273
|
/// ready nor paints.
|
|
1269
1274
|
var splashMaxSeconds: Double = 3
|