@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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/runtime/app/app.ts +55 -54
  3. package/runtime/app/shell/android-helpers.ts +6 -6
  4. package/runtime/app/shell/background-worker.android.ts +47 -0
  5. package/runtime/app/shell/background-worker.ios.ts +5 -0
  6. package/runtime/app/shell/background-worker.ts +7 -0
  7. package/runtime/app/shell/banner.ts +9 -4
  8. package/runtime/app/shell/bridge.ts +2 -0
  9. package/runtime/app/shell/custom-webview.android.ts +50 -23
  10. package/runtime/app/shell/custom-webview.ios.ts +90 -78
  11. package/runtime/app/shell/devmenu.ts +2 -2
  12. package/runtime/app/shell/env.ts +1 -3
  13. package/runtime/app/shell/events.ts +5 -4
  14. package/runtime/app/shell/handlers-android.ts +16 -12
  15. package/runtime/app/shell/handlers-background.ts +57 -78
  16. package/runtime/app/shell/handlers-billing.ts +90 -82
  17. package/runtime/app/shell/handlers-extended.ts +141 -110
  18. package/runtime/app/shell/handlers-fs.ts +53 -41
  19. package/runtime/app/shell/handlers-health.ts +27 -19
  20. package/runtime/app/shell/handlers-media.ts +8 -14
  21. package/runtime/app/shell/handlers-oauth.ts +19 -19
  22. package/runtime/app/shell/handlers-parity.ts +118 -89
  23. package/runtime/app/shell/handlers-push.ts +5 -3
  24. package/runtime/app/shell/handlers-reviews.ts +5 -6
  25. package/runtime/app/shell/handlers-scanner.ts +36 -38
  26. package/runtime/app/shell/handlers-speech.ts +43 -49
  27. package/runtime/app/shell/handlers-system.ts +1 -1
  28. package/runtime/app/shell/handlers.ts +4 -3
  29. package/runtime/app/shell/ios-frameworks.d.ts +9 -0
  30. package/runtime/app/shell/ios-ui-delegate.ts +112 -108
  31. package/runtime/app/shell/native-log.ts +1 -3
  32. package/runtime/app/shell/status-bar.ts +7 -4
  33. package/runtime/app/shell/toast.ts +1 -1
  34. package/runtime/references.d.ts +6 -0
  35. package/runtime/tsconfig.json +1 -0
  36. package/src/cli.ts +43 -18
  37. package/src/derive.ts +8 -0
  38. package/templates/ci/github/workflows/appwrap-pr.yml +3 -3
  39. package/templates/ci/github/workflows/appwrap-release-android.yml +2 -2
  40. 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 android: any, androidx: any, java: any;
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 as any,
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 as any,
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
- export function createUiDelegate(): any {
16
- const DelegateClass = (NSObject as any).extend(
17
- {
18
- // iOS 15+: WKUIDelegate media capture permission.
19
- webViewRequestMediaCapturePermissionForOriginInitiatedByFrameTypeDecisionHandler(
20
- _webView: WKWebView,
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
- // Preserve the behavior NS's own UIDelegate gave us (we fully replace it):
37
- // target=_blank / window.open has no new window in a shell → load it in place.
38
- webViewCreateWebViewWithConfigurationForNavigationActionWindowFeatures(
39
- webView: WKWebView,
40
- _configuration: any,
41
- navigationAction: WKNavigationAction,
42
- _windowFeatures: any
43
- ): WKWebView | null {
44
- const frame = navigationAction.targetFrame;
45
- if (!frame || !frame.mainFrame) webView.loadRequest(navigationAction.request);
46
- return null;
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
- webViewRunJavaScriptAlertPanelWithMessageInitiatedByFrameCompletionHandler(
50
- _webView: WKWebView,
51
- message: string,
52
- _frame: any,
53
- completionHandler: () => void
54
- ): void {
55
- Utils.dispatchToMainThread(() => {
56
- const alert = UIAlertController.alertControllerWithTitleMessagePreferredStyle(
57
- null as any,
58
- String(message ?? ''),
59
- UIAlertControllerStyle.Alert
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
- webViewRunJavaScriptConfirmPanelWithMessageInitiatedByFrameCompletionHandler(
69
- _webView: WKWebView,
70
- message: string,
71
- _frame: any,
72
- completionHandler: (ok: boolean) => void
73
- ): void {
74
- Utils.dispatchToMainThread(() => {
75
- const alert = UIAlertController.alertControllerWithTitleMessagePreferredStyle(
76
- null as any,
77
- String(message ?? ''),
78
- UIAlertControllerStyle.Alert
79
- );
80
- alert.addAction(
81
- UIAlertAction.actionWithTitleStyleHandler('Cancel', UIAlertActionStyle.Cancel, () =>
82
- completionHandler(false)
83
- )
84
- );
85
- alert.addAction(
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
- webViewRunJavaScriptTextInputPanelWithPromptDefaultTextInitiatedByFrameCompletionHandler(
93
- _webView: WKWebView,
94
- prompt: string,
95
- defaultText: string,
96
- _frame: any,
97
- completionHandler: (text: string | null) => void
98
- ): void {
99
- Utils.dispatchToMainThread(() => {
100
- const alert = UIAlertController.alertControllerWithTitleMessagePreferredStyle(
101
- null as any,
102
- String(prompt ?? ''),
103
- UIAlertControllerStyle.Alert
104
- );
105
- let field: UITextField | null = null;
106
- alert.addTextFieldWithConfigurationHandler((tf: UITextField) => {
107
- tf.text = String(defaultText ?? '');
108
- field = tf;
109
- });
110
- alert.addAction(
111
- UIAlertAction.actionWithTitleStyleHandler('Cancel', UIAlertActionStyle.Cancel, () =>
112
- completionHandler(null)
113
- )
114
- );
115
- alert.addAction(
116
- UIAlertAction.actionWithTitleStyleHandler('OK', UIAlertActionStyle.Default, () =>
117
- completionHandler(field ? String(field.text ?? '') : '')
118
- )
119
- );
120
- present(alert);
121
- });
122
- },
123
- },
124
- { protocols: [WKUIDelegate] }
125
- );
126
- return DelegateClass.new();
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*/, null as any);
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
- (Utils as any).android.enableEdgeToEdge(activity, {
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
- (webView as any).android?.evaluateJavascript(js, null);
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 = (global as any).android.view.WindowInsets.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 = (decorView as any).getWindowInsetsController?.();
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 = (android as any).widget.Toast;
7
+ const Toast = android.widget.Toast;
8
8
  Toast.makeText(
9
9
  Utils.android.getApplicationContext(),
10
10
  message,
@@ -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" />
@@ -5,6 +5,7 @@
5
5
  "moduleResolution": "bundler",
6
6
  "lib": ["es2021", "dom"],
7
7
  "experimentalDecorators": true,
8
+ "noImplicitAny": true,
8
9
  "removeComments": false,
9
10
  "noEmitHelpers": true,
10
11
  "noEmitOnError": true,
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): Record<string, any> | null {
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: any) {
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: any;
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: any) {
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
- if (lastErr?.stderr) console.warn(` ${String(lastErr.stderr).trim()}`);
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
- if (lastErr?.stderr) console.error(String(lastErr.stderr).trim());
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: any) => d?.hardwareProperties?.platform === 'iOS'
1158
+ .filter((d) => d?.hardwareProperties?.platform === 'iOS'
1134
1159
  && d?.connectionProperties?.tunnelState !== 'unavailable')
1135
- .map((d: any) => ({
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 any).prompt(`Select device [1-${devices.length}]: `);
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: any) {
1227
- const log = `${e?.stdout ?? ''}${e?.stderr ?? ''}`;
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: any) {
1290
- process.stderr.write(`${e?.stdout ?? ''}${e?.stderr ?? ''}`);
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: any) {
1304
- const log = `${e?.stdout ?? ''}${e?.stderr ?? ''}`;
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@v4
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@v4
20
+ - uses: actions/checkout@v5
21
21
  - uses: oven-sh/setup-bun@v2
22
- - uses: actions/setup-node@v4
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@v4
25
+ - uses: actions/checkout@v5
26
26
  - uses: oven-sh/setup-bun@v2
27
- - uses: actions/setup-node@v4
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@v4
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@v4
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.