@livx.cc/appwrap 0.28.0 → 0.28.2
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/package.json +1 -1
- package/runtime/app/app.ts +55 -54
- package/runtime/app/shell/android-helpers.ts +6 -6
- package/runtime/app/shell/background-worker.android.ts +47 -0
- package/runtime/app/shell/background-worker.ios.ts +5 -0
- package/runtime/app/shell/background-worker.ts +7 -0
- package/runtime/app/shell/banner.ts +9 -4
- package/runtime/app/shell/bridge.ts +2 -0
- package/runtime/app/shell/custom-webview.android.ts +50 -23
- package/runtime/app/shell/custom-webview.ios.ts +90 -78
- package/runtime/app/shell/devmenu.ts +2 -2
- package/runtime/app/shell/env.ts +1 -3
- package/runtime/app/shell/events.ts +5 -4
- package/runtime/app/shell/handlers-android.ts +16 -12
- package/runtime/app/shell/handlers-background.ts +57 -78
- package/runtime/app/shell/handlers-billing.ts +90 -82
- package/runtime/app/shell/handlers-extended.ts +141 -110
- package/runtime/app/shell/handlers-fs.ts +53 -41
- package/runtime/app/shell/handlers-health.ts +27 -19
- package/runtime/app/shell/handlers-media.ts +8 -14
- package/runtime/app/shell/handlers-oauth.ts +19 -19
- package/runtime/app/shell/handlers-parity.ts +118 -89
- package/runtime/app/shell/handlers-push.ts +5 -3
- package/runtime/app/shell/handlers-reviews.ts +5 -6
- package/runtime/app/shell/handlers-scanner.ts +36 -38
- package/runtime/app/shell/handlers-speech.ts +43 -49
- package/runtime/app/shell/handlers-system.ts +1 -1
- package/runtime/app/shell/handlers.ts +4 -3
- package/runtime/app/shell/ios-frameworks.d.ts +9 -0
- package/runtime/app/shell/ios-ui-delegate.ts +112 -108
- package/runtime/app/shell/native-log.ts +1 -3
- package/runtime/app/shell/status-bar.ts +7 -4
- package/runtime/app/shell/toast.ts +1 -1
- package/runtime/references.d.ts +6 -0
- package/runtime/tsconfig.json +1 -0
- package/src/cli.ts +43 -18
- package/src/derive.ts +8 -0
- package/templates/ci/github/workflows/appwrap-pr.yml +3 -3
- package/templates/ci/github/workflows/appwrap-release-android.yml +2 -2
- package/templates/ci/github/workflows/appwrap-release-ios.yml +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Application, ApplicationSettings, Color, Utils, isAndroid, isIOS } from '@nativescript/core';
|
|
2
2
|
import { bridge } from './bridge';
|
|
3
3
|
|
|
4
|
-
declare const
|
|
4
|
+
declare const androidx: any; // no NS types: androidx
|
|
5
5
|
|
|
6
6
|
const err = (code: string, message: string) => Object.assign(new Error(message), { code });
|
|
7
7
|
|
|
@@ -80,7 +80,7 @@ export function registerHandlers(): void {
|
|
|
80
80
|
const items = NSMutableArray.new();
|
|
81
81
|
if (content) items.addObject(content);
|
|
82
82
|
const controller = UIActivityViewController.alloc().initWithActivityItemsApplicationActivities(
|
|
83
|
-
items
|
|
83
|
+
items,
|
|
84
84
|
null
|
|
85
85
|
);
|
|
86
86
|
const rootVC = Utils.ios.getRootViewController();
|
|
@@ -108,6 +108,7 @@ export function registerHandlers(): void {
|
|
|
108
108
|
if (text) items.addObject(text);
|
|
109
109
|
const tmp = NSTemporaryDirectory();
|
|
110
110
|
(files ?? []).forEach((f, i) => {
|
|
111
|
+
// 0 = no decoding options; the enum has no zero member, so cast the raw bitmask value.
|
|
111
112
|
const data = NSData.alloc().initWithBase64EncodedStringOptions(f.base64, 0 as unknown as NSDataBase64DecodingOptions);
|
|
112
113
|
if (!data) return;
|
|
113
114
|
const filePath = tmp + i + '-' + (f.name || 'file'); // index-prefix: avoid same-name collisions in one call
|
|
@@ -115,7 +116,7 @@ export function registerHandlers(): void {
|
|
|
115
116
|
items.addObject(NSURL.fileURLWithPath(filePath));
|
|
116
117
|
});
|
|
117
118
|
const controller = UIActivityViewController.alloc().initWithActivityItemsApplicationActivities(
|
|
118
|
-
items
|
|
119
|
+
items,
|
|
119
120
|
null
|
|
120
121
|
);
|
|
121
122
|
const rootVC = Utils.ios.getRootViewController();
|
|
@@ -187,6 +188,6 @@ function safeJson(s: string): unknown {
|
|
|
187
188
|
|
|
188
189
|
function vibrateAndroid(ms: number): void {
|
|
189
190
|
const context = Utils.android.getApplicationContext();
|
|
190
|
-
const vibrator = context.getSystemService(android.content.Context.VIBRATOR_SERVICE);
|
|
191
|
+
const vibrator = context.getSystemService(android.content.Context.VIBRATOR_SERVICE) as android.os.Vibrator;
|
|
191
192
|
vibrator?.vibrate(ms);
|
|
192
193
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Neutralizer stub — intentionally EMPTY. Do NOT add `declare const X: any` here.
|
|
2
|
+
//
|
|
3
|
+
// Real iOS framework types come from the @nativescript/types-ios references in `references.d.ts`.
|
|
4
|
+
// This file exists ONLY so `appwrap sync` copies it over `native/app/shell/ios-frameworks.d.ts`,
|
|
5
|
+
// overwriting a stale PRE-`references.d.ts` generated relic of the same name. That relic declared
|
|
6
|
+
// every framework symbol as `declare const X: any` (a VALUE); once handlers annotate params with the
|
|
7
|
+
// real types (e.g. `mm: CMMotionManager`), the value-shadow makes the build throw TS2749 ("refers to
|
|
8
|
+
// a value, used as a type"). Shipping this empty stub self-heals any native/ on the next sync.
|
|
9
|
+
export {};
|
|
@@ -12,118 +12,122 @@ import { Utils } from '@nativescript/core';
|
|
|
12
12
|
*
|
|
13
13
|
* Returns a retained delegate instance (the WKWebView holds uiDelegate weakly).
|
|
14
14
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
_origin: any,
|
|
22
|
-
_frame: any,
|
|
23
|
-
_type: any,
|
|
24
|
-
decisionHandler: (decision: number) => void
|
|
25
|
-
): void {
|
|
26
|
-
// WKPermissionDecision.Grant === 1 — silently grant (the app already holds the
|
|
27
|
-
// OS-level mic/camera permission; media.ensurePermission established TCC).
|
|
28
|
-
// NOTE: this only ever sees capabilities the build DECLARED — a request for an
|
|
29
|
-
// UNDECLARED capability is rejected upstream in JS (mediaCaptureGuardJs), because
|
|
30
|
-
// WebKit asks TCC to authorize the device AROUND this callback and a missing usage
|
|
31
|
-
// string is a hard process-kill a native `deny` here cannot prevent. So denying
|
|
32
|
-
// here is pointless for the crash; the real guard must be (and is) at the JS layer.
|
|
33
|
-
decisionHandler(1);
|
|
34
|
-
},
|
|
15
|
+
@NativeClass()
|
|
16
|
+
class AppwrapUiDelegate extends NSObject implements WKUIDelegate {
|
|
17
|
+
static ObjCProtocols = [WKUIDelegate];
|
|
18
|
+
static new(): AppwrapUiDelegate {
|
|
19
|
+
return <AppwrapUiDelegate>super.new();
|
|
20
|
+
}
|
|
35
21
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
22
|
+
// iOS 15+: WKUIDelegate media capture permission.
|
|
23
|
+
webViewRequestMediaCapturePermissionForOriginInitiatedByFrameTypeDecisionHandler(
|
|
24
|
+
_webView: WKWebView,
|
|
25
|
+
_origin: WKSecurityOrigin,
|
|
26
|
+
_frame: WKFrameInfo,
|
|
27
|
+
_type: WKMediaCaptureType,
|
|
28
|
+
decisionHandler: (decision: WKPermissionDecision) => void
|
|
29
|
+
): void {
|
|
30
|
+
// WKPermissionDecision.Grant === 1 — silently grant (the app already holds the
|
|
31
|
+
// OS-level mic/camera permission; media.ensurePermission established TCC).
|
|
32
|
+
// NOTE: this only ever sees capabilities the build DECLARED — a request for an
|
|
33
|
+
// UNDECLARED capability is rejected upstream in JS (mediaCaptureGuardJs), because
|
|
34
|
+
// WebKit asks TCC to authorize the device AROUND this callback and a missing usage
|
|
35
|
+
// string is a hard process-kill a native `deny` here cannot prevent. So denying
|
|
36
|
+
// here is pointless for the crash; the real guard must be (and is) at the JS layer.
|
|
37
|
+
decisionHandler(WKPermissionDecision.Grant);
|
|
38
|
+
}
|
|
48
39
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
alert.addAction(
|
|
62
|
-
UIAlertAction.actionWithTitleStyleHandler('OK', UIAlertActionStyle.Default, () => completionHandler())
|
|
63
|
-
);
|
|
64
|
-
present(alert);
|
|
65
|
-
});
|
|
66
|
-
},
|
|
40
|
+
// Preserve the behavior NS's own UIDelegate gave us (we fully replace it):
|
|
41
|
+
// target=_blank / window.open has no new window in a shell → load it in place.
|
|
42
|
+
webViewCreateWebViewWithConfigurationForNavigationActionWindowFeatures(
|
|
43
|
+
webView: WKWebView,
|
|
44
|
+
_configuration: WKWebViewConfiguration,
|
|
45
|
+
navigationAction: WKNavigationAction,
|
|
46
|
+
_windowFeatures: WKWindowFeatures
|
|
47
|
+
): WKWebView {
|
|
48
|
+
const frame = navigationAction.targetFrame;
|
|
49
|
+
if (!frame || !frame.mainFrame) webView.loadRequest(navigationAction.request);
|
|
50
|
+
return null as unknown as WKWebView;
|
|
51
|
+
}
|
|
67
52
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
UIAlertAction.actionWithTitleStyleHandler('OK', UIAlertActionStyle.Default, () => completionHandler(true))
|
|
87
|
-
);
|
|
88
|
-
present(alert);
|
|
89
|
-
});
|
|
90
|
-
},
|
|
53
|
+
webViewRunJavaScriptAlertPanelWithMessageInitiatedByFrameCompletionHandler(
|
|
54
|
+
_webView: WKWebView,
|
|
55
|
+
message: string,
|
|
56
|
+
_frame: WKFrameInfo,
|
|
57
|
+
completionHandler: () => void
|
|
58
|
+
): void {
|
|
59
|
+
Utils.dispatchToMainThread(() => {
|
|
60
|
+
const alert = UIAlertController.alertControllerWithTitleMessagePreferredStyle(
|
|
61
|
+
null as unknown as string,
|
|
62
|
+
String(message ?? ''),
|
|
63
|
+
UIAlertControllerStyle.Alert
|
|
64
|
+
);
|
|
65
|
+
alert.addAction(
|
|
66
|
+
UIAlertAction.actionWithTitleStyleHandler('OK', UIAlertActionStyle.Default, () => completionHandler())
|
|
67
|
+
);
|
|
68
|
+
present(alert);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
91
71
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
72
|
+
webViewRunJavaScriptConfirmPanelWithMessageInitiatedByFrameCompletionHandler(
|
|
73
|
+
_webView: WKWebView,
|
|
74
|
+
message: string,
|
|
75
|
+
_frame: WKFrameInfo,
|
|
76
|
+
completionHandler: (ok: boolean) => void
|
|
77
|
+
): void {
|
|
78
|
+
Utils.dispatchToMainThread(() => {
|
|
79
|
+
const alert = UIAlertController.alertControllerWithTitleMessagePreferredStyle(
|
|
80
|
+
null as unknown as string,
|
|
81
|
+
String(message ?? ''),
|
|
82
|
+
UIAlertControllerStyle.Alert
|
|
83
|
+
);
|
|
84
|
+
alert.addAction(
|
|
85
|
+
UIAlertAction.actionWithTitleStyleHandler('Cancel', UIAlertActionStyle.Cancel, () =>
|
|
86
|
+
completionHandler(false)
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
alert.addAction(
|
|
90
|
+
UIAlertAction.actionWithTitleStyleHandler('OK', UIAlertActionStyle.Default, () => completionHandler(true))
|
|
91
|
+
);
|
|
92
|
+
present(alert);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
webViewRunJavaScriptTextInputPanelWithPromptDefaultTextInitiatedByFrameCompletionHandler(
|
|
97
|
+
_webView: WKWebView,
|
|
98
|
+
prompt: string,
|
|
99
|
+
defaultText: string,
|
|
100
|
+
_frame: WKFrameInfo,
|
|
101
|
+
completionHandler: (text: string) => void
|
|
102
|
+
): void {
|
|
103
|
+
Utils.dispatchToMainThread(() => {
|
|
104
|
+
const alert = UIAlertController.alertControllerWithTitleMessagePreferredStyle(
|
|
105
|
+
null as unknown as string,
|
|
106
|
+
String(prompt ?? ''),
|
|
107
|
+
UIAlertControllerStyle.Alert
|
|
108
|
+
);
|
|
109
|
+
let field: UITextField | null = null;
|
|
110
|
+
alert.addTextFieldWithConfigurationHandler((tf: UITextField) => {
|
|
111
|
+
tf.text = String(defaultText ?? '');
|
|
112
|
+
field = tf;
|
|
113
|
+
});
|
|
114
|
+
alert.addAction(
|
|
115
|
+
UIAlertAction.actionWithTitleStyleHandler('Cancel', UIAlertActionStyle.Cancel, () =>
|
|
116
|
+
completionHandler(null as unknown as string)
|
|
117
|
+
)
|
|
118
|
+
);
|
|
119
|
+
alert.addAction(
|
|
120
|
+
UIAlertAction.actionWithTitleStyleHandler('OK', UIAlertActionStyle.Default, () =>
|
|
121
|
+
completionHandler(field ? String(field.text ?? '') : '')
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
present(alert);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function createUiDelegate(): WKUIDelegate {
|
|
130
|
+
return AppwrapUiDelegate.new();
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
function present(alert: UIAlertController): void {
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { knownFolders, path as nsPath } from '@nativescript/core';
|
|
2
2
|
|
|
3
|
-
declare const NSString: any;
|
|
4
|
-
|
|
5
3
|
/**
|
|
6
4
|
* Single debug log sink → Documents/appwrap-web.log. Both the forwarded WebView console
|
|
7
5
|
* (custom-webview) and native shell diagnostics (handlers) write here, because NativeScript's
|
|
@@ -15,7 +13,7 @@ export function appwrapNativeLog(line: string): void {
|
|
|
15
13
|
_log += line + '\n';
|
|
16
14
|
if (_log.length > 160000) _log = _log.slice(-120000);
|
|
17
15
|
const p = nsPath.join(knownFolders.documents().path, 'appwrap-web.log');
|
|
18
|
-
NSString.stringWithString(_log).writeToFileAtomicallyEncodingError(p, true, 4 /*NSUTF8
|
|
16
|
+
NSString.stringWithString(_log).writeToFileAtomicallyEncodingError(p, true, 4 /*NSUTF8*/);
|
|
19
17
|
} catch {
|
|
20
18
|
/* best-effort */
|
|
21
19
|
}
|
|
@@ -28,7 +28,7 @@ export function enableAndroidEdgeToEdge(): void {
|
|
|
28
28
|
try {
|
|
29
29
|
// Explicit transparent status-bar colors (light + dark) so the page background — not the NS
|
|
30
30
|
// page backgroundColor — shows under the status bar. Nav bar keeps NS's default subtle scrim.
|
|
31
|
-
|
|
31
|
+
Utils.android.enableEdgeToEdge(activity, {
|
|
32
32
|
statusBarLightColor: transparent,
|
|
33
33
|
statusBarDarkColor: transparent,
|
|
34
34
|
});
|
|
@@ -55,6 +55,7 @@ export function wireAndroidSafeArea(webView: View): void {
|
|
|
55
55
|
// Un-box the layout that wraps the WebView so it (and its child WebView) fills the window UNDER the
|
|
56
56
|
// bars. 'dont-apply' = NS won't pad it with the system-bar insets.
|
|
57
57
|
const layout = (webView.parent as View) ?? webView;
|
|
58
|
+
// androidOverflowEdge: not in the public View typings — an NS edge-to-edge layout hint set dynamically.
|
|
58
59
|
(layout as any).androidOverflowEdge = 'dont-apply';
|
|
59
60
|
|
|
60
61
|
const density = Screen.mainScreen.scale || 1; // physical px per dp
|
|
@@ -65,7 +66,8 @@ export function wireAndroidSafeArea(webView: View): void {
|
|
|
65
66
|
`s.setProperty('--saie-right','${r}px');s.setProperty('--saie-bottom','${b}px');` +
|
|
66
67
|
`})(document.documentElement.style);`;
|
|
67
68
|
try {
|
|
68
|
-
|
|
69
|
+
// webView.android is typed `any` by NS core; on Android it's an android.webkit.WebView.
|
|
70
|
+
webView.android?.evaluateJavascript(js, null);
|
|
69
71
|
} catch (e) {
|
|
70
72
|
console.warn('AppWrap: safe-area inject failed', e);
|
|
71
73
|
}
|
|
@@ -82,7 +84,7 @@ export function wireAndroidSafeArea(webView: View): void {
|
|
|
82
84
|
const decor = Application.android?.startActivity?.getWindow()?.getDecorView();
|
|
83
85
|
const wi = decor?.getRootWindowInsets?.();
|
|
84
86
|
if (!wi) return false;
|
|
85
|
-
const Type =
|
|
87
|
+
const Type = android.view.WindowInsets.Type;
|
|
86
88
|
const ins = wi.getInsets(Type.systemBars() | Type.displayCutout());
|
|
87
89
|
inject(dp(ins.left), dp(ins.top), dp(ins.right), dp(ins.bottom));
|
|
88
90
|
return ins.top + ins.bottom + ins.left + ins.right > 0;
|
|
@@ -132,12 +134,13 @@ export function applyThemeColor(color: string): void {
|
|
|
132
134
|
/** 'light' = white icons/text (for dark backgrounds), 'dark' = black. */
|
|
133
135
|
export function setStatusBarStyle(style: 'light' | 'dark'): void {
|
|
134
136
|
if (isIOS) {
|
|
137
|
+
// statusBarStyle is a CSS-backed Page property, not on the public Page type — set dynamically.
|
|
135
138
|
if (currentPage) (currentPage as any).statusBarStyle = style;
|
|
136
139
|
} else if (isAndroid) {
|
|
137
140
|
const window = Application.android?.startActivity?.getWindow();
|
|
138
141
|
if (!window) return;
|
|
139
142
|
const decorView = window.getDecorView();
|
|
140
|
-
const controller =
|
|
143
|
+
const controller = decorView.getWindowInsetsController?.();
|
|
141
144
|
if (controller) {
|
|
142
145
|
const APPEARANCE_LIGHT_STATUS_BARS = 8; // WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
|
|
143
146
|
// Android "light status bar" = dark icons; invert from our naming
|
|
@@ -4,7 +4,7 @@ import { Utils, isAndroid, isIOS } from '@nativescript/core';
|
|
|
4
4
|
export function showToast(message: string, duration: 'short' | 'long' = 'short'): void {
|
|
5
5
|
const ms = duration === 'long' ? 3500 : 2000;
|
|
6
6
|
if (isAndroid) {
|
|
7
|
-
const Toast =
|
|
7
|
+
const Toast = android.widget.Toast;
|
|
8
8
|
Toast.makeText(
|
|
9
9
|
Utils.android.getApplicationContext(),
|
|
10
10
|
message,
|
package/runtime/references.d.ts
CHANGED
|
@@ -14,4 +14,10 @@
|
|
|
14
14
|
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!StoreKit.d.ts" />
|
|
15
15
|
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!LocalAuthentication.d.ts" />
|
|
16
16
|
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!PhotosUI.d.ts" />
|
|
17
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!_LocationEssentials.d.ts" />
|
|
17
18
|
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!CoreLocation.d.ts" />
|
|
19
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!AVFoundation.d.ts" />
|
|
20
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!Speech.d.ts" />
|
|
21
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!BackgroundTasks.d.ts" />
|
|
22
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!AuthenticationServices.d.ts" />
|
|
23
|
+
/// <reference path="./node_modules/@nativescript/types-ios/lib/ios/objc-x86_64/objc!Photos.d.ts" />
|
package/runtime/tsconfig.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -29,6 +29,21 @@ import {
|
|
|
29
29
|
stampPlistBackgroundTasks,
|
|
30
30
|
stampPlistOrientations,
|
|
31
31
|
} from './derive';
|
|
32
|
+
import type { WebManifest } from './derive';
|
|
33
|
+
|
|
34
|
+
/** What `child_process.execFileSync` attaches to the Error it throws on a non-zero exit
|
|
35
|
+
* (stdout/stderr are Buffer with the default encoding, string when `encoding` is set). */
|
|
36
|
+
interface ExecError extends Error {
|
|
37
|
+
stdout?: string | Buffer;
|
|
38
|
+
stderr?: string | Buffer;
|
|
39
|
+
status?: number | null;
|
|
40
|
+
}
|
|
41
|
+
const asExecError = (e: unknown): ExecError => (e ?? {}) as ExecError;
|
|
42
|
+
/** Combined stdout+stderr captured on an exec failure (empty string when none). */
|
|
43
|
+
const execErrText = (e: unknown): string => {
|
|
44
|
+
const err = asExecError(e);
|
|
45
|
+
return `${err.stdout ?? ''}${err.stderr ?? ''}`;
|
|
46
|
+
};
|
|
32
47
|
|
|
33
48
|
/** Marketing version → a monotonic integer build (0.2.1 → 201; 1.4.12 → 10412). Stable & increasing
|
|
34
49
|
* across semver bumps so store re-uploads are always accepted without a manual bump. */
|
|
@@ -301,15 +316,15 @@ function parseArgs(argv: string[]) {
|
|
|
301
316
|
}
|
|
302
317
|
|
|
303
318
|
/** Parse the PWA's web manifest (manifest.json / .webmanifest) from the dist dir, or null. */
|
|
304
|
-
function loadManifest(cwd: string, cfg: AppwrapConfig):
|
|
319
|
+
function loadManifest(cwd: string, cfg: AppwrapConfig): WebManifest | null {
|
|
305
320
|
const dist = resolve(cwd, cfg.pwaDist);
|
|
306
321
|
for (const name of ['manifest.json', 'manifest.webmanifest']) {
|
|
307
322
|
const mf = join(dist, name);
|
|
308
323
|
if (!existsSync(mf)) continue;
|
|
309
324
|
try {
|
|
310
325
|
return JSON.parse(readFileSync(mf, 'utf8'));
|
|
311
|
-
} catch (e:
|
|
312
|
-
console.warn(`⚠ Could not parse ${name}: ${e.message}`);
|
|
326
|
+
} catch (e: unknown) {
|
|
327
|
+
console.warn(`⚠ Could not parse ${name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
313
328
|
}
|
|
314
329
|
}
|
|
315
330
|
return null;
|
|
@@ -876,13 +891,13 @@ function vendorBackendAssets(www: string, cfg: AppwrapConfig): void {
|
|
|
876
891
|
// Retry a couple times (the backend can briefly reset under deploy/cold-start), and on total
|
|
877
892
|
// failure fall back to the cached copy if one exists rather than breaking the whole sync.
|
|
878
893
|
let ok = false;
|
|
879
|
-
let lastErr:
|
|
894
|
+
let lastErr: unknown;
|
|
880
895
|
for (let attempt = 0; attempt < 3 && !ok; attempt++) {
|
|
881
896
|
try {
|
|
882
897
|
execFileSync('curl', ['-fsSL', '--retry', '2', url, '-o', tmp], { stdio: 'pipe' });
|
|
883
898
|
cpSync(tmp, dest);
|
|
884
899
|
ok = true;
|
|
885
|
-
} catch (e:
|
|
900
|
+
} catch (e: unknown) {
|
|
886
901
|
lastErr = e;
|
|
887
902
|
}
|
|
888
903
|
}
|
|
@@ -891,10 +906,12 @@ function vendorBackendAssets(www: string, cfg: AppwrapConfig): void {
|
|
|
891
906
|
console.log(` vendor ← ${url}`);
|
|
892
907
|
} else if (existsSync(dest) && readFileSync(dest).length > 0) {
|
|
893
908
|
console.warn(`⚠ vendor fetch failed: ${url} — using cached copy (backend unreachable).`);
|
|
894
|
-
|
|
909
|
+
const err = execErrText(lastErr);
|
|
910
|
+
if (err) console.warn(` ${err.trim()}`);
|
|
895
911
|
} else {
|
|
896
912
|
console.error(`✖ vendor fetch failed: ${url} (backend reachable? path correct?) — no cached copy to fall back to`);
|
|
897
|
-
|
|
913
|
+
const err = execErrText(lastErr);
|
|
914
|
+
if (err) console.error(err.trim());
|
|
898
915
|
process.exit(1);
|
|
899
916
|
}
|
|
900
917
|
}
|
|
@@ -1121,19 +1138,27 @@ async function build(cwd: string, flags: Record<string, string>, positionals: st
|
|
|
1121
1138
|
|
|
1122
1139
|
interface DeviceInfo { id: string; name: string; model: string; transport: string }
|
|
1123
1140
|
|
|
1141
|
+
/** The subset of a `xcrun devicectl list devices --json-output` device entry appwrap reads. */
|
|
1142
|
+
interface DevicectlDevice {
|
|
1143
|
+
identifier?: string;
|
|
1144
|
+
deviceProperties?: { name?: string };
|
|
1145
|
+
hardwareProperties?: { platform?: string; marketingName?: string; productType?: string };
|
|
1146
|
+
connectionProperties?: { tunnelState?: string; transportType?: string };
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1124
1149
|
/** Discover usable physical iOS devices via devicectl (USB + network). Excludes 'unavailable'
|
|
1125
1150
|
* tunnels and non-iOS (watch). Returns [] if none. */
|
|
1126
1151
|
function listIosDevices(): DeviceInfo[] {
|
|
1127
1152
|
const out = join(tmpdir(), `appwrap-devices-${process.pid}.json`);
|
|
1128
1153
|
try {
|
|
1129
1154
|
execFileSync('xcrun', ['devicectl', 'list', 'devices', '--json-output', out], { stdio: 'pipe' });
|
|
1130
|
-
const j = JSON.parse(readFileSync(out, 'utf8'));
|
|
1155
|
+
const j = JSON.parse(readFileSync(out, 'utf8')) as { result?: { devices?: DevicectlDevice[] } };
|
|
1131
1156
|
rmSync(out, { force: true });
|
|
1132
1157
|
return (j?.result?.devices ?? [])
|
|
1133
|
-
.filter((d
|
|
1158
|
+
.filter((d) => d?.hardwareProperties?.platform === 'iOS'
|
|
1134
1159
|
&& d?.connectionProperties?.tunnelState !== 'unavailable')
|
|
1135
|
-
.map((d
|
|
1136
|
-
id: d.identifier,
|
|
1160
|
+
.map((d) => ({
|
|
1161
|
+
id: d.identifier ?? '',
|
|
1137
1162
|
name: d?.deviceProperties?.name ?? '(unknown)',
|
|
1138
1163
|
model: d?.hardwareProperties?.marketingName ?? d?.hardwareProperties?.productType ?? '',
|
|
1139
1164
|
transport: d?.connectionProperties?.transportType ?? '',
|
|
@@ -1160,7 +1185,7 @@ function pickDevice(devices: DeviceInfo[], explicitId?: string): DeviceInfo {
|
|
|
1160
1185
|
}
|
|
1161
1186
|
console.log('Multiple devices connected:');
|
|
1162
1187
|
devices.forEach((d, i) => console.log(` ${i + 1}) ${d.name} — ${d.model || 'iPhone'} [${d.transport}]`));
|
|
1163
|
-
const ans = (globalThis as
|
|
1188
|
+
const ans = (globalThis as { prompt(msg?: string): string | null }).prompt(`Select device [1-${devices.length}]: `);
|
|
1164
1189
|
const idx = Number(ans) - 1;
|
|
1165
1190
|
if (!Number.isInteger(idx) || idx < 0 || idx >= devices.length) {
|
|
1166
1191
|
console.error('✖ Invalid selection.'); process.exit(1);
|
|
@@ -1223,8 +1248,8 @@ async function deploy(cwd: string, flags: Record<string, string>, positionals: s
|
|
|
1223
1248
|
execFileSync('xcrun', ['devicectl', 'device', 'install', 'app', '--timeout', '25', '--device', device.id, ipaPath], { encoding: 'utf8', stdio: ['inherit', 'pipe', 'pipe'] })
|
|
1224
1249
|
);
|
|
1225
1250
|
process.stdout.write(out);
|
|
1226
|
-
} catch (e:
|
|
1227
|
-
const log =
|
|
1251
|
+
} catch (e: unknown) {
|
|
1252
|
+
const log = execErrText(e);
|
|
1228
1253
|
process.stderr.write(log);
|
|
1229
1254
|
if (/maximum number of installed apps|MIInstallerErrorDomain error 13|ApplicationVerificationFailed/.test(log)) {
|
|
1230
1255
|
// Free Apple developer profile caps a device at 3 app IDs — a sibling appwrap/WDA build often eats a slot.
|
|
@@ -1286,8 +1311,8 @@ function usbmuxInstall(ipaPath: string): boolean {
|
|
|
1286
1311
|
const out = execFileSync('ideviceinstaller', ['install', ipaPath], { encoding: 'utf8', stdio: ['inherit', 'pipe', 'pipe'] });
|
|
1287
1312
|
process.stdout.write(out);
|
|
1288
1313
|
return /Complete|Installed/i.test(out);
|
|
1289
|
-
} catch (e:
|
|
1290
|
-
process.stderr.write(
|
|
1314
|
+
} catch (e: unknown) {
|
|
1315
|
+
process.stderr.write(execErrText(e));
|
|
1291
1316
|
return false;
|
|
1292
1317
|
}
|
|
1293
1318
|
}
|
|
@@ -1300,8 +1325,8 @@ function withUnlockRetry<T>(label: string, run: () => T, tries = 40, delayMs = 3
|
|
|
1300
1325
|
for (let i = 0; ; i++) {
|
|
1301
1326
|
try {
|
|
1302
1327
|
return run();
|
|
1303
|
-
} catch (e:
|
|
1304
|
-
const log =
|
|
1328
|
+
} catch (e: unknown) {
|
|
1329
|
+
const log = execErrText(e);
|
|
1305
1330
|
if (/maximum number of installed apps|MIInstallerErrorDomain error 13|ApplicationVerificationFailed/.test(log)) throw e;
|
|
1306
1331
|
// devicectl/CoreDevice tunnel STUCK (it switches to a [wired] path and hangs) is NOT a lock —
|
|
1307
1332
|
// retrying as "waiting for unlock" is pointless + misleading. Re-throw so the caller can fall back.
|
package/src/derive.ts
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
/** Normalized appwrap orientation — the three lock states the native shells can express. */
|
|
8
8
|
export type Orientation = 'portrait' | 'landscape' | 'any';
|
|
9
9
|
|
|
10
|
+
/** A single PWA web-manifest icon entry (the subset appwrap reads). */
|
|
11
|
+
export interface WebManifestIcon {
|
|
12
|
+
src: string;
|
|
13
|
+
sizes?: string;
|
|
14
|
+
purpose?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
/** The PWA web-manifest fields appwrap derives from (a subset; everything optional). */
|
|
11
18
|
export interface WebManifest {
|
|
12
19
|
name?: string;
|
|
@@ -14,6 +21,7 @@ export interface WebManifest {
|
|
|
14
21
|
background_color?: string;
|
|
15
22
|
theme_color?: string;
|
|
16
23
|
orientation?: string;
|
|
24
|
+
icons?: WebManifestIcon[];
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
/**
|
|
@@ -7,7 +7,7 @@ jobs:
|
|
|
7
7
|
web:
|
|
8
8
|
runs-on: ubuntu-latest
|
|
9
9
|
steps:
|
|
10
|
-
- uses: actions/checkout@
|
|
10
|
+
- uses: actions/checkout@v5
|
|
11
11
|
- uses: oven-sh/setup-bun@v2
|
|
12
12
|
- run: bun install
|
|
13
13
|
- run: bun test || echo "no tests"
|
|
@@ -17,9 +17,9 @@ jobs:
|
|
|
17
17
|
runs-on: macos-15
|
|
18
18
|
needs: web
|
|
19
19
|
steps:
|
|
20
|
-
- uses: actions/checkout@
|
|
20
|
+
- uses: actions/checkout@v5
|
|
21
21
|
- uses: oven-sh/setup-bun@v2
|
|
22
|
-
- uses: actions/setup-node@
|
|
22
|
+
- uses: actions/setup-node@v5
|
|
23
23
|
with: { node-version: 22 }
|
|
24
24
|
- run: bun install && bun run build
|
|
25
25
|
# Pin global tools — unpinned installs can grab a breaking release mid-flight. Bump deliberately.
|
|
@@ -22,9 +22,9 @@ jobs:
|
|
|
22
22
|
play:
|
|
23
23
|
runs-on: ubuntu-latest
|
|
24
24
|
steps:
|
|
25
|
-
- uses: actions/checkout@
|
|
25
|
+
- uses: actions/checkout@v5
|
|
26
26
|
- uses: oven-sh/setup-bun@v2
|
|
27
|
-
- uses: actions/setup-node@
|
|
27
|
+
- uses: actions/setup-node@v5
|
|
28
28
|
with: { node-version: 22 }
|
|
29
29
|
- uses: actions/setup-java@v4
|
|
30
30
|
with: { distribution: temurin, java-version: 17 }
|
|
@@ -24,12 +24,12 @@ jobs:
|
|
|
24
24
|
testflight:
|
|
25
25
|
runs-on: macos-15
|
|
26
26
|
steps:
|
|
27
|
-
- uses: actions/checkout@
|
|
27
|
+
- uses: actions/checkout@v5
|
|
28
28
|
# Apple requires the iOS 26 SDK (Xcode 26+) for uploads — runners may default to older Xcode.
|
|
29
29
|
- uses: maxim-lobanov/setup-xcode@v1
|
|
30
30
|
with: { xcode-version: latest-stable }
|
|
31
31
|
- uses: oven-sh/setup-bun@v2
|
|
32
|
-
- uses: actions/setup-node@
|
|
32
|
+
- uses: actions/setup-node@v5
|
|
33
33
|
with: { node-version: 22 }
|
|
34
34
|
- run: bun install && bun run build
|
|
35
35
|
# Pin global tools — unpinned installs can grab a breaking release mid-flight. Bump deliberately.
|