@craft-native/ios 0.0.124 → 0.0.126
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 +11 -5
- package/dist/cli.js +87 -5
- package/dist/index.d.ts +49 -1
- package/dist/index.js +150 -4
- package/package.json +1 -1
- package/templates/CraftApp.swift +111 -13
- package/templates/CraftHybrid.swift +1199 -0
- package/templates/CraftNativeActions.swift +213 -1
- package/templates/CraftNativeFlatList.swift +150 -21
- package/templates/CraftNativeScreen.swift +1683 -307
package/README.md
CHANGED
|
@@ -64,7 +64,10 @@ Build native iOS apps with web technologies using Craft.
|
|
|
64
64
|
|
|
65
65
|
The iOS support is built into the main `craft` CLI:
|
|
66
66
|
|
|
67
|
-
`
|
|
67
|
+
`Pressable`, `TouchableOpacity`, and `TouchableHighlight` render as native
|
|
68
|
+
pressable flow containers. `Modal` renders a parent-scoped blocking layer in native mode, with `visible`,
|
|
69
|
+
`transparent`, `animationType`, `onShow`, `onDismiss`, and `onRequestClose`
|
|
70
|
+
support. `FlatList` item roots receive the same `onLayout` callback as ordinary native
|
|
68
71
|
views when rows are materialized or recycled.
|
|
69
72
|
|
|
70
73
|
```bash
|
|
@@ -109,16 +112,19 @@ In Xcode:
|
|
|
109
112
|
|
|
110
113
|
### Experimental WebView-free screen
|
|
111
114
|
|
|
112
|
-
|
|
115
|
+
Keyboard frame changes update the native controller's additional safe area, so
|
|
116
|
+
focused inputs and responsive `onLayout` measurements move with the keyboard.
|
|
117
|
+
|
|
118
|
+
The opt-in `native` renderer hosts compiled `.stx` screens in JavaScriptCore and renders their `View`, `SafeAreaView`, `Text`, `Button`, `Link`, `TextInput`, `Picker`, `Switch`, `Slider`, `ActivityIndicator`, `Image`, `ScrollView`, and recycling `FlatList` nodes as UIKit controls. `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. `Picker` renders `<select>`/`<option>` values with `onValueChange` or `onChange`; `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. 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).
|
|
113
119
|
|
|
114
120
|
```bash
|
|
115
121
|
craft ios init NativeDemo --renderer native --output ./ios-native
|
|
116
|
-
stx
|
|
122
|
+
stx native compile Screen.stx --format bundle --output ./screen.js
|
|
117
123
|
craft ios build --output ./ios-native --native-bundle ./screen.js
|
|
118
124
|
craft ios run --output ./ios-native --simulator
|
|
119
125
|
```
|
|
120
126
|
|
|
121
|
-
`stx
|
|
127
|
+
`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.
|
|
122
128
|
|
|
123
129
|
Native screen updates use the versioned mutation protocol when the bundle
|
|
124
130
|
supports it and fall back to whole-document rendering for older bundles.
|
|
@@ -133,7 +139,7 @@ the [Native STX guide](../../docs/guides/native-stx.md). On macOS,
|
|
|
133
139
|
`bun packages/ios/scripts/test-native-render.ts` runs the renderer's unit and
|
|
134
140
|
simulator UI tests.
|
|
135
141
|
|
|
136
|
-
For multiple native screens, configure named `.stx` routes and an initial screen in `
|
|
142
|
+
For multiple native screens, configure named `.stx` routes and an initial screen in `native.config.json` (see [`fixtures/native-navigation`](./fixtures/native-navigation)). Compile without a positional file: `stx native compile --format bundle --output ./screen.js`. A screen can call `craft.navigation.push('details', { id: 7 })`, `.replace('home')`, or `.back()`; the destination reads `craft.route.params`. Craft uses a native navigation stack, so the back button and edge-swipe reveal the existing controller and retain its JavaScript and input state. `bun packages/ios/scripts/test-native-navigation.ts` compiles the fixture and runs its simulator test; the default WebView renderer is unchanged.
|
|
137
143
|
|
|
138
144
|
The optional Liquid Glass tab-bar styling is compiler-gated for Xcode 26/Swift 6.2 and later. Older SDKs compile the same tab bar with the material fallback, so generated projects remain buildable on Xcode 16.
|
|
139
145
|
|
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,62 @@ var CRAFT_VERSION = (() => {
|
|
|
16
16
|
return "0.0.0";
|
|
17
17
|
}
|
|
18
18
|
})();
|
|
19
|
+
function normalizeNativePath(raw) {
|
|
20
|
+
if (typeof raw !== "string")
|
|
21
|
+
return null;
|
|
22
|
+
let path = raw;
|
|
23
|
+
const cut = path.search(/[?#]/);
|
|
24
|
+
if (cut >= 0)
|
|
25
|
+
path = path.slice(0, cut);
|
|
26
|
+
if (!path.startsWith("/"))
|
|
27
|
+
return null;
|
|
28
|
+
path = path.replace(/\/{2,}/g, "/");
|
|
29
|
+
if (path.length > 1 && path.endsWith("/"))
|
|
30
|
+
path = path.slice(0, -1);
|
|
31
|
+
return path;
|
|
32
|
+
}
|
|
33
|
+
function routeSegments(path) {
|
|
34
|
+
return path === "/" ? [] : path.slice(1).split("/");
|
|
35
|
+
}
|
|
36
|
+
function isHybrid(config) {
|
|
37
|
+
return (config.renderer ?? "web") === "web" && Object.keys(config.nativeScreens ?? {}).length > 0;
|
|
38
|
+
}
|
|
39
|
+
function hybridConfigProblems(config) {
|
|
40
|
+
const problems = [];
|
|
41
|
+
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
|
+
for (const [pattern, screen] of screens) {
|
|
45
|
+
if (normalizeNativePath(pattern) !== pattern || /[?#]/.test(pattern))
|
|
46
|
+
problems.push(`nativeScreens path "${pattern}" must be an absolute path with no query, hash or trailing slash.`);
|
|
47
|
+
if (routeSegments(normalizeNativePath(pattern) ?? "/").slice(0, -1).includes("*"))
|
|
48
|
+
problems.push(`nativeScreens path "${pattern}" may only end in *.`);
|
|
49
|
+
if (typeof screen !== "string" || !/^[A-Za-z_$][\w$-]*$/.test(screen))
|
|
50
|
+
problems.push(`nativeScreens screen "${String(screen)}" for "${pattern}" must be a screen name from the native bundle.`);
|
|
51
|
+
}
|
|
52
|
+
const ids = new Set;
|
|
53
|
+
for (const tab of config.tabs ?? []) {
|
|
54
|
+
if (!tab || typeof tab.id !== "string" || !tab.id || typeof tab.title !== "string" || !tab.title)
|
|
55
|
+
problems.push("Each tab needs an id (its root path) and a title.");
|
|
56
|
+
else if (ids.has(tab.id))
|
|
57
|
+
problems.push(`Tab "${tab.id}" is listed twice.`);
|
|
58
|
+
else
|
|
59
|
+
ids.add(tab.id);
|
|
60
|
+
}
|
|
61
|
+
for (const [key, target] of Object.entries(config.shareStorage ?? {})) {
|
|
62
|
+
if (!key || typeof target !== "string" || !target)
|
|
63
|
+
problems.push(`shareStorage "${key}" needs the Keychain key native screens read it under.`);
|
|
64
|
+
}
|
|
65
|
+
return problems;
|
|
66
|
+
}
|
|
67
|
+
function installNativeBundle(source, output) {
|
|
68
|
+
if (!existsSync(source))
|
|
69
|
+
throw new Error(`Native bundle not found: ${source}`);
|
|
70
|
+
const destination = join(output, "dist", "native-screen.js");
|
|
71
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
72
|
+
if (resolve(source) !== resolve(destination))
|
|
73
|
+
cpSync(source, destination);
|
|
74
|
+
}
|
|
19
75
|
var DEFAULT_CONFIG = {
|
|
20
76
|
renderer: "web",
|
|
21
77
|
version: "1.0.0",
|
|
@@ -648,6 +704,12 @@ async function init(options) {
|
|
|
648
704
|
}
|
|
649
705
|
if (config.enableBackgroundLocation)
|
|
650
706
|
config.enableGeolocation = true;
|
|
707
|
+
const problems = hybridConfigProblems(config);
|
|
708
|
+
if (problems.length)
|
|
709
|
+
throw new Error(problems.join(`
|
|
710
|
+
`));
|
|
711
|
+
if (Object.keys(config.shareStorage ?? {}).length)
|
|
712
|
+
config.enableSecureStorage = true;
|
|
651
713
|
writeFileSync(join(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
652
714
|
const swiftTemplate = readFileSync(join(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
|
|
653
715
|
const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION);
|
|
@@ -657,6 +719,7 @@ async function init(options) {
|
|
|
657
719
|
cpSync(join(TEMPLATES_DIR, "CraftNativeActions.swift"), join(output, "Sources", "CraftNativeActions.swift"));
|
|
658
720
|
cpSync(join(TEMPLATES_DIR, "CraftNativeMutation.swift"), join(output, "Sources", "CraftNativeMutation.swift"));
|
|
659
721
|
cpSync(join(TEMPLATES_DIR, "CraftNativeUI.swift"), join(output, "Sources", "CraftNativeUI.swift"));
|
|
722
|
+
cpSync(join(TEMPLATES_DIR, "CraftHybrid.swift"), join(output, "Sources", "CraftHybrid.swift"));
|
|
660
723
|
const infoPlistTemplate = readFileSync(join(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
|
|
661
724
|
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>
|
|
662
725
|
<true/>
|
|
@@ -749,12 +812,18 @@ ${nativeDependencies.join(`
|
|
|
749
812
|
writeFileSync(join(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
750
813
|
}
|
|
751
814
|
writeFileSync(join(output, "dist", "index.html"), renderOfflinePage(config));
|
|
815
|
+
if (isHybrid(config)) {
|
|
816
|
+
if (config.nativeBundle)
|
|
817
|
+
installNativeBundle(config.nativeBundle, output);
|
|
818
|
+
else
|
|
819
|
+
console.warn(" \u26A0 nativeScreens is set without nativeBundle: every path opens in the web view until `craft ios build --native-bundle` supplies one.");
|
|
820
|
+
}
|
|
752
821
|
console.log("\u2705 Project initialized");
|
|
753
822
|
console.log("");
|
|
754
823
|
console.log("Next steps:");
|
|
755
824
|
console.log(` 1. cd ${output}`);
|
|
756
825
|
if (config.renderer === "native") {
|
|
757
|
-
console.log(" 2. Compile your screen: stx
|
|
826
|
+
console.log(" 2. Compile your screen: stx native compile Screen.stx --format bundle --output screen.js");
|
|
758
827
|
console.log(" 3. Run: craft ios build --native-bundle screen.js");
|
|
759
828
|
} else {
|
|
760
829
|
console.log(" 2. Add your web content to dist/index.html");
|
|
@@ -785,11 +854,15 @@ async function build(options) {
|
|
|
785
854
|
cpSync(nativeBundlePath, destination);
|
|
786
855
|
}
|
|
787
856
|
if (!existsSync(join(output, "dist", "native-screen.js"))) {
|
|
788
|
-
throw new Error("Native iOS mode needs dist/native-screen.js. Compile a .stx screen with stx
|
|
857
|
+
throw new Error("Native iOS mode needs dist/native-screen.js. Compile a .stx screen with `stx native compile` first.");
|
|
789
858
|
}
|
|
790
|
-
} else if (nativeBundlePath) {
|
|
791
|
-
throw new Error('--native-bundle requires renderer: "native" in craft.config.json.');
|
|
859
|
+
} else if (nativeBundlePath && !isHybrid(config)) {
|
|
860
|
+
throw new Error('--native-bundle requires renderer: "native", or nativeScreens, in craft.config.json.');
|
|
792
861
|
}
|
|
862
|
+
const problems = hybridConfigProblems(config);
|
|
863
|
+
if (problems.length)
|
|
864
|
+
throw new Error(problems.join(`
|
|
865
|
+
`));
|
|
793
866
|
if (devServer) {
|
|
794
867
|
config.devServerURL = devServer;
|
|
795
868
|
const origin = new URL(devServer).origin;
|
|
@@ -801,6 +874,15 @@ async function build(options) {
|
|
|
801
874
|
syncWebAssets(htmlPath, output);
|
|
802
875
|
console.log(` Synced: ${htmlPath} \u2192 dist/`);
|
|
803
876
|
}
|
|
877
|
+
if (isHybrid(config)) {
|
|
878
|
+
const bundle = nativeBundlePath ?? config.nativeBundle;
|
|
879
|
+
if (bundle) {
|
|
880
|
+
installNativeBundle(bundle, output);
|
|
881
|
+
console.log(` Native screens: ${bundle} \u2192 dist/native-screen.js`);
|
|
882
|
+
} else if (!existsSync(join(output, "dist", "native-screen.js"))) {
|
|
883
|
+
console.warn(" \u26A0 nativeScreens is set but there is no native bundle: every path opens in the web view.");
|
|
884
|
+
}
|
|
885
|
+
}
|
|
804
886
|
await refreshRuntime(output, options.runtimeDir);
|
|
805
887
|
if (!generateProject)
|
|
806
888
|
return;
|
|
@@ -962,7 +1044,7 @@ Options:
|
|
|
962
1044
|
-t, --team-id <id> Apple Developer Team ID
|
|
963
1045
|
--renderer <kind> iOS renderer: web (default) or native
|
|
964
1046
|
--html-path <path> Path to HTML file or directory
|
|
965
|
-
--native-bundle <path> JavaScript bundle
|
|
1047
|
+
--native-bundle <path> JavaScript bundle from \`stx native compile\`
|
|
966
1048
|
-d, --dev-server <url> Development server URL
|
|
967
1049
|
-o, --output <dir> Output directory (default: ./ios)
|
|
968
1050
|
-s, --simulator Run on simulator instead of device
|
package/dist/index.d.ts
CHANGED
|
@@ -144,7 +144,55 @@ export interface CraftConfig {
|
|
|
144
144
|
requestTimeoutSeconds?: number;
|
|
145
145
|
orientations?: Array<'portrait' | 'landscape-left' | 'landscape-right' | 'portrait-upside-down'>;
|
|
146
146
|
deviceFamilies?: Array<'iphone' | 'ipad'>;
|
|
147
|
+
/**
|
|
148
|
+
* Hybrid: app paths drawn as native stx screens instead of in the
|
|
149
|
+
* web view, each to the screen in the native bundle that draws it, e.g.
|
|
150
|
+
* `{ "/m": "Today", "/m/calendar": "Calendar" }`. A segment may be a
|
|
151
|
+
* parameter (`/m/workout/:id`) and the last one `*`. Needs `renderer: "web"`
|
|
152
|
+
* and `nativeBundle`; every other path stays in the web view, and a path
|
|
153
|
+
* whose screen is missing or throws falls back to it.
|
|
154
|
+
*/
|
|
155
|
+
nativeScreens?: Record<string, string>;
|
|
156
|
+
/** Hybrid: the bundle `stx native compile --format bundle` wrote, copied to `dist/native-screen.js`. */
|
|
157
|
+
nativeBundle?: string;
|
|
158
|
+
/**
|
|
159
|
+
* The native tab bar, shown from the first frame, before the page loads.
|
|
160
|
+
* The page's own `tabBar.set` replaces it later. Each `id` is the tab's
|
|
161
|
+
* root path, as the page's tab links name it.
|
|
162
|
+
*/
|
|
163
|
+
tabs?: CraftTab[];
|
|
164
|
+
/**
|
|
165
|
+
* Hybrid: web-storage keys the page writes (localStorage or
|
|
166
|
+
* sessionStorage) that native screens need, mirrored into the Keychain
|
|
167
|
+
* under the name given, e.g. `{ "auth_token": "auth.token" }` for
|
|
168
|
+
* `craft.secureStorage.getSync('auth.token')`. Turns on secure storage.
|
|
169
|
+
*/
|
|
170
|
+
shareStorage?: Record<string, string>;
|
|
171
|
+
}
|
|
172
|
+
export interface CraftTab {
|
|
173
|
+
/** The tab's root path; comes back to the page in `craftTabSelect`. */
|
|
174
|
+
id: string;
|
|
175
|
+
title: string;
|
|
176
|
+
/** An SF Symbol name. */
|
|
177
|
+
symbol?: string;
|
|
178
|
+
}
|
|
179
|
+
/** `/a//b/?q#h` → `/a/b`, the way the shell and the page compare paths. Null for a relative path. */
|
|
180
|
+
export declare function normalizeNativePath(raw: string): string | null;
|
|
181
|
+
export interface NativePathMatch {
|
|
182
|
+
path: string;
|
|
183
|
+
screen: string;
|
|
184
|
+
params: Record<string, string>;
|
|
147
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* The native screen for a path, by the rules the shell (`CraftHybridRoutes`)
|
|
188
|
+
* and the page script apply: static segments win over `:params`, which win
|
|
189
|
+
* over a trailing `*`; query items become params under the path's own.
|
|
190
|
+
*/
|
|
191
|
+
export declare function matchNativePath(nativeScreens: Record<string, string> | undefined, raw: string): NativePathMatch | null;
|
|
192
|
+
/** Whether a config mixes native screens into its web page. */
|
|
193
|
+
export declare function isHybrid(config: Pick<CraftConfig, 'renderer' | 'nativeScreens'>): boolean;
|
|
194
|
+
/** What is wrong with a config's hybrid and tab settings, one sentence each. */
|
|
195
|
+
export declare function hybridConfigProblems(config: Pick<CraftConfig, 'renderer' | 'nativeScreens' | 'tabs' | 'shareStorage'>): string[];
|
|
148
196
|
export interface CraftBackgroundRefresh {
|
|
149
197
|
enabled: boolean;
|
|
150
198
|
/** The BGTaskScheduler identifier. Default `<bundleId>.refresh`. */
|
|
@@ -191,7 +239,7 @@ export interface InitOptions {
|
|
|
191
239
|
}
|
|
192
240
|
export interface BuildOptions {
|
|
193
241
|
htmlPath?: string;
|
|
194
|
-
/** JavaScript bundle emitted by `stx
|
|
242
|
+
/** JavaScript bundle emitted by `stx native compile Screen.stx --format bundle`. */
|
|
195
243
|
nativeBundlePath?: string;
|
|
196
244
|
devServer?: string;
|
|
197
245
|
output: string;
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,122 @@ var CRAFT_VERSION = (() => {
|
|
|
11
11
|
return "0.0.0";
|
|
12
12
|
}
|
|
13
13
|
})();
|
|
14
|
+
function normalizeNativePath(raw) {
|
|
15
|
+
if (typeof raw !== "string")
|
|
16
|
+
return null;
|
|
17
|
+
let path = raw;
|
|
18
|
+
const cut = path.search(/[?#]/);
|
|
19
|
+
if (cut >= 0)
|
|
20
|
+
path = path.slice(0, cut);
|
|
21
|
+
if (!path.startsWith("/"))
|
|
22
|
+
return null;
|
|
23
|
+
path = path.replace(/\/{2,}/g, "/");
|
|
24
|
+
if (path.length > 1 && path.endsWith("/"))
|
|
25
|
+
path = path.slice(0, -1);
|
|
26
|
+
return path;
|
|
27
|
+
}
|
|
28
|
+
function routeSegments(path) {
|
|
29
|
+
return path === "/" ? [] : path.slice(1).split("/");
|
|
30
|
+
}
|
|
31
|
+
function matchNativePath(nativeScreens, raw) {
|
|
32
|
+
const path = normalizeNativePath(raw);
|
|
33
|
+
if (path === null)
|
|
34
|
+
return null;
|
|
35
|
+
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) }));
|
|
36
|
+
const wild = (segments) => segments.at(-1) === "*" ? 1 : 0;
|
|
37
|
+
const fixed = (segments) => segments.filter((s) => s !== "*" && !s.startsWith(":")).length;
|
|
38
|
+
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));
|
|
39
|
+
const decode = (value) => {
|
|
40
|
+
try {
|
|
41
|
+
return decodeURIComponent(value);
|
|
42
|
+
} catch {
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const parts = routeSegments(path);
|
|
47
|
+
for (const route of routes) {
|
|
48
|
+
const params = {};
|
|
49
|
+
let matched = true;
|
|
50
|
+
for (let i = 0;i < route.segments.length; i++) {
|
|
51
|
+
const segment = route.segments[i];
|
|
52
|
+
if (segment === "*") {
|
|
53
|
+
params["*"] = parts.slice(i).join("/");
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
if (i >= parts.length) {
|
|
57
|
+
matched = false;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
if (segment.startsWith(":")) {
|
|
61
|
+
if (!parts[i]) {
|
|
62
|
+
matched = false;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
params[segment.slice(1)] = decode(parts[i]);
|
|
66
|
+
} else if (segment !== parts[i]) {
|
|
67
|
+
matched = false;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (!matched)
|
|
72
|
+
continue;
|
|
73
|
+
if (route.segments.at(-1) !== "*" && route.segments.length !== parts.length)
|
|
74
|
+
continue;
|
|
75
|
+
const query = raw.indexOf("?");
|
|
76
|
+
if (query >= 0) {
|
|
77
|
+
for (const item of raw.slice(query + 1).split("#")[0].split("&")) {
|
|
78
|
+
if (!item)
|
|
79
|
+
continue;
|
|
80
|
+
const [rawKey = "", ...rest] = item.split("=");
|
|
81
|
+
const key = decode(rawKey);
|
|
82
|
+
if (!key || key in params)
|
|
83
|
+
continue;
|
|
84
|
+
params[key] = decode(rest.join("=").replace(/\+/g, " "));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { path, screen: route.screen, params };
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
function isHybrid(config) {
|
|
92
|
+
return (config.renderer ?? "web") === "web" && Object.keys(config.nativeScreens ?? {}).length > 0;
|
|
93
|
+
}
|
|
94
|
+
function hybridConfigProblems(config) {
|
|
95
|
+
const problems = [];
|
|
96
|
+
const screens = Object.entries(config.nativeScreens ?? {});
|
|
97
|
+
if (screens.length && config.renderer === "native")
|
|
98
|
+
problems.push('nativeScreens mixes native screens into a web app; it needs renderer: "web" (renderer: "native" is all native already).');
|
|
99
|
+
for (const [pattern, screen] of screens) {
|
|
100
|
+
if (normalizeNativePath(pattern) !== pattern || /[?#]/.test(pattern))
|
|
101
|
+
problems.push(`nativeScreens path "${pattern}" must be an absolute path with no query, hash or trailing slash.`);
|
|
102
|
+
if (routeSegments(normalizeNativePath(pattern) ?? "/").slice(0, -1).includes("*"))
|
|
103
|
+
problems.push(`nativeScreens path "${pattern}" may only end in *.`);
|
|
104
|
+
if (typeof screen !== "string" || !/^[A-Za-z_$][\w$-]*$/.test(screen))
|
|
105
|
+
problems.push(`nativeScreens screen "${String(screen)}" for "${pattern}" must be a screen name from the native bundle.`);
|
|
106
|
+
}
|
|
107
|
+
const ids = new Set;
|
|
108
|
+
for (const tab of config.tabs ?? []) {
|
|
109
|
+
if (!tab || typeof tab.id !== "string" || !tab.id || typeof tab.title !== "string" || !tab.title)
|
|
110
|
+
problems.push("Each tab needs an id (its root path) and a title.");
|
|
111
|
+
else if (ids.has(tab.id))
|
|
112
|
+
problems.push(`Tab "${tab.id}" is listed twice.`);
|
|
113
|
+
else
|
|
114
|
+
ids.add(tab.id);
|
|
115
|
+
}
|
|
116
|
+
for (const [key, target] of Object.entries(config.shareStorage ?? {})) {
|
|
117
|
+
if (!key || typeof target !== "string" || !target)
|
|
118
|
+
problems.push(`shareStorage "${key}" needs the Keychain key native screens read it under.`);
|
|
119
|
+
}
|
|
120
|
+
return problems;
|
|
121
|
+
}
|
|
122
|
+
function installNativeBundle(source, output) {
|
|
123
|
+
if (!existsSync(source))
|
|
124
|
+
throw new Error(`Native bundle not found: ${source}`);
|
|
125
|
+
const destination = join(output, "dist", "native-screen.js");
|
|
126
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
127
|
+
if (resolve(source) !== resolve(destination))
|
|
128
|
+
cpSync(source, destination);
|
|
129
|
+
}
|
|
14
130
|
var DEFAULT_CONFIG = {
|
|
15
131
|
renderer: "web",
|
|
16
132
|
version: "1.0.0",
|
|
@@ -643,6 +759,12 @@ async function init(options) {
|
|
|
643
759
|
}
|
|
644
760
|
if (config.enableBackgroundLocation)
|
|
645
761
|
config.enableGeolocation = true;
|
|
762
|
+
const problems = hybridConfigProblems(config);
|
|
763
|
+
if (problems.length)
|
|
764
|
+
throw new Error(problems.join(`
|
|
765
|
+
`));
|
|
766
|
+
if (Object.keys(config.shareStorage ?? {}).length)
|
|
767
|
+
config.enableSecureStorage = true;
|
|
646
768
|
writeFileSync(join(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
647
769
|
const swiftTemplate = readFileSync(join(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
|
|
648
770
|
const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_VERSION);
|
|
@@ -652,6 +774,7 @@ async function init(options) {
|
|
|
652
774
|
cpSync(join(TEMPLATES_DIR, "CraftNativeActions.swift"), join(output, "Sources", "CraftNativeActions.swift"));
|
|
653
775
|
cpSync(join(TEMPLATES_DIR, "CraftNativeMutation.swift"), join(output, "Sources", "CraftNativeMutation.swift"));
|
|
654
776
|
cpSync(join(TEMPLATES_DIR, "CraftNativeUI.swift"), join(output, "Sources", "CraftNativeUI.swift"));
|
|
777
|
+
cpSync(join(TEMPLATES_DIR, "CraftHybrid.swift"), join(output, "Sources", "CraftHybrid.swift"));
|
|
655
778
|
const infoPlistTemplate = readFileSync(join(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
|
|
656
779
|
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>
|
|
657
780
|
<true/>
|
|
@@ -744,12 +867,18 @@ ${nativeDependencies.join(`
|
|
|
744
867
|
writeFileSync(join(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
745
868
|
}
|
|
746
869
|
writeFileSync(join(output, "dist", "index.html"), renderOfflinePage(config));
|
|
870
|
+
if (isHybrid(config)) {
|
|
871
|
+
if (config.nativeBundle)
|
|
872
|
+
installNativeBundle(config.nativeBundle, output);
|
|
873
|
+
else
|
|
874
|
+
console.warn(" \u26A0 nativeScreens is set without nativeBundle: every path opens in the web view until `craft ios build --native-bundle` supplies one.");
|
|
875
|
+
}
|
|
747
876
|
console.log("\u2705 Project initialized");
|
|
748
877
|
console.log("");
|
|
749
878
|
console.log("Next steps:");
|
|
750
879
|
console.log(` 1. cd ${output}`);
|
|
751
880
|
if (config.renderer === "native") {
|
|
752
|
-
console.log(" 2. Compile your screen: stx
|
|
881
|
+
console.log(" 2. Compile your screen: stx native compile Screen.stx --format bundle --output screen.js");
|
|
753
882
|
console.log(" 3. Run: craft ios build --native-bundle screen.js");
|
|
754
883
|
} else {
|
|
755
884
|
console.log(" 2. Add your web content to dist/index.html");
|
|
@@ -780,11 +909,15 @@ async function build(options) {
|
|
|
780
909
|
cpSync(nativeBundlePath, destination);
|
|
781
910
|
}
|
|
782
911
|
if (!existsSync(join(output, "dist", "native-screen.js"))) {
|
|
783
|
-
throw new Error("Native iOS mode needs dist/native-screen.js. Compile a .stx screen with stx
|
|
912
|
+
throw new Error("Native iOS mode needs dist/native-screen.js. Compile a .stx screen with `stx native compile` first.");
|
|
784
913
|
}
|
|
785
|
-
} else if (nativeBundlePath) {
|
|
786
|
-
throw new Error('--native-bundle requires renderer: "native" in craft.config.json.');
|
|
914
|
+
} else if (nativeBundlePath && !isHybrid(config)) {
|
|
915
|
+
throw new Error('--native-bundle requires renderer: "native", or nativeScreens, in craft.config.json.');
|
|
787
916
|
}
|
|
917
|
+
const problems = hybridConfigProblems(config);
|
|
918
|
+
if (problems.length)
|
|
919
|
+
throw new Error(problems.join(`
|
|
920
|
+
`));
|
|
788
921
|
if (devServer) {
|
|
789
922
|
config.devServerURL = devServer;
|
|
790
923
|
const origin = new URL(devServer).origin;
|
|
@@ -796,6 +929,15 @@ async function build(options) {
|
|
|
796
929
|
syncWebAssets(htmlPath, output);
|
|
797
930
|
console.log(` Synced: ${htmlPath} \u2192 dist/`);
|
|
798
931
|
}
|
|
932
|
+
if (isHybrid(config)) {
|
|
933
|
+
const bundle = nativeBundlePath ?? config.nativeBundle;
|
|
934
|
+
if (bundle) {
|
|
935
|
+
installNativeBundle(bundle, output);
|
|
936
|
+
console.log(` Native screens: ${bundle} \u2192 dist/native-screen.js`);
|
|
937
|
+
} else if (!existsSync(join(output, "dist", "native-screen.js"))) {
|
|
938
|
+
console.warn(" \u26A0 nativeScreens is set but there is no native bundle: every path opens in the web view.");
|
|
939
|
+
}
|
|
940
|
+
}
|
|
799
941
|
await refreshRuntime(output, options.runtimeDir);
|
|
800
942
|
if (!generateProject)
|
|
801
943
|
return;
|
|
@@ -944,8 +1086,12 @@ export {
|
|
|
944
1086
|
pickSimulator,
|
|
945
1087
|
orderSimulators,
|
|
946
1088
|
open,
|
|
1089
|
+
normalizeNativePath,
|
|
1090
|
+
matchNativePath,
|
|
1091
|
+
isHybrid,
|
|
947
1092
|
installRuntime,
|
|
948
1093
|
init,
|
|
1094
|
+
hybridConfigProblems,
|
|
949
1095
|
build,
|
|
950
1096
|
bootSimulator,
|
|
951
1097
|
backgroundRefreshIdentifier,
|