@livx.cc/appwrap 0.37.0 → 0.38.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livx.cc/appwrap",
3
- "version": "0.37.0",
3
+ "version": "0.38.0",
4
4
  "description": "Wrap any PWA into a native app with native capabilities (appwrap runtime + @livx.cc/native-kit).",
5
5
  "license": "MIT",
6
6
  "author": "Elya Livshitz",
package/src/cli.ts CHANGED
@@ -22,6 +22,7 @@ import type * as CapManifest from '../../../runtime/app/shell/capabilities.manif
22
22
  import type { AppwrapConfig } from './config';
23
23
  import {
24
24
  androidScreenOrientation,
25
+ applyBuildNumberFlag,
25
26
  iosOrientations,
26
27
  mergeManifest,
27
28
  resolveBuildNumber,
@@ -525,6 +526,20 @@ function stampTeamId(outDir: string, cfg: AppwrapConfig, ctx?: { cwd: string; co
525
526
  writeFileSync(xcconfig, src);
526
527
  }
527
528
 
529
+ /** Stamp TARGETED_DEVICE_FAMILY into build.xcconfig from cfg.targetedDevices. `'iphone'` → `1`
530
+ * (iPhone-only → UIDeviceFamily=[1], so the App Store doesn't require iPad screenshots);
531
+ * `'universal'`/unset → `1,2` (NativeScript's default). Idempotent: replaces any prior value. */
532
+ function stampDeviceFamily(outDir: string, cfg: AppwrapConfig): void {
533
+ const xcconfig = join(outDir, 'App_Resources/iOS/build.xcconfig');
534
+ if (!existsSync(xcconfig)) return;
535
+ const value = cfg.targetedDevices === 'iphone' ? '1' : '1,2';
536
+ let src = readFileSync(xcconfig, 'utf8');
537
+ src = /TARGETED_DEVICE_FAMILY\s*=/.test(src)
538
+ ? src.replace(/TARGETED_DEVICE_FAMILY\s*=\s*[^;\n]*;?/, `TARGETED_DEVICE_FAMILY = ${value};`)
539
+ : src + `\nTARGETED_DEVICE_FAMILY = ${value};\n`;
540
+ writeFileSync(xcconfig, src);
541
+ }
542
+
528
543
  /** Wire a StoreKit config file for LOCAL iOS IAP testing (no App Store Connect needed).
529
544
  * NativeScript copies App_Resources/iOS/* into the generated project and adds it as a file
530
545
  * reference — but it never points the scheme at it, so StoreKit has no catalog. We (1) drop the
@@ -1036,6 +1051,7 @@ function regenerateCore(cwd: string, outDir: string, cfg: AppwrapConfig, opts: {
1036
1051
  stampNativeScriptConfig(outDir, cfg);
1037
1052
  stampIOSDisplayName(outDir, cfg, req);
1038
1053
  stampTeamId(outDir, cfg, { cwd, configPath: resolveConfigPath(cwd, opts.flags ?? {}) });
1054
+ stampDeviceFamily(outDir, cfg);
1039
1055
  stampAndroidAppName(outDir, cfg, req);
1040
1056
  stampAndroidVersion(outDir, cfg);
1041
1057
  stampAndroidGradleDeps(outDir, req.androidGradleDeps);
@@ -1158,6 +1174,14 @@ async function build(cwd: string, flags: Record<string, string>, positionals: st
1158
1174
  console.error(`✖ Wrapper not found at ${outDir} — run \`appwrap init\` first`);
1159
1175
  process.exit(1);
1160
1176
  }
1177
+ // --build-number → process.env.APPWRAP_BUILD_NUMBER before sync() stamps the plist/gradle (same
1178
+ // mechanism `release` uses). Env-var path still honored by sync()'s buildNumberOf when no flag.
1179
+ try {
1180
+ applyBuildNumberFlag(flags['build-number']);
1181
+ } catch (e) {
1182
+ console.error(`✖ ${(e as Error).message}`);
1183
+ process.exit(1);
1184
+ }
1161
1185
  // Make sure the wrapper reflects the latest config + PWA before compiling (also validates the config).
1162
1186
  await sync(cwd, flags);
1163
1187
 
@@ -1207,11 +1231,18 @@ async function build(cwd: string, flags: Record<string, string>, positionals: st
1207
1231
  * --build-number <n> set the store CFBundleVersion (sets APPWRAP_BUILD_NUMBER for the lane)
1208
1232
  *
1209
1233
  * Signing/ASC config is read from env by the lane (ASC_KEY_ID / ASC_ISSUER_ID / ASC_KEY_P8 /
1210
- * MATCH_GIT_URL / MATCH_PASSWORD) — secrets never live in appwrap.config. */
1211
- async function release(cwd: string, flags: Record<string, string>, positionals: string[]): Promise<void> {
1234
+ * MATCH_GIT_URL / MATCH_PASSWORD) — secrets never live in appwrap.config.
1235
+ *
1236
+ * `appwrap submit ios` reuses this same body with `lane: 'release'` → fastlane `:release`
1237
+ * (`upload_to_app_store`): a binary-only App Store production promote. Metadata/screenshots stay in
1238
+ * ASC; pass `--submit-for-review` to also submit the build for review. */
1239
+ async function release(cwd: string, flags: Record<string, string>, positionals: string[], lane: 'beta' | 'release' = 'beta'): Promise<void> {
1240
+ const submit = lane === 'release';
1241
+ const cmd = submit ? 'submit' : 'release';
1212
1242
  const platform = positionals[0];
1213
1243
  if (platform !== 'ios') {
1214
- console.error('Usage: appwrap release ios [--server-url <url>] [--env <name>] [--build-number <n>] [--config <path>] [--out native]\n' +
1244
+ console.error(`Usage: appwrap ${cmd} ios [--server-url <url>] [--env <name>] [--build-number <n>]` +
1245
+ (submit ? ' [--submit-for-review]' : '') + ' [--config <path>] [--out native]\n' +
1215
1246
  ' (Android: `appwrap build android --release --aab` then `fastlane android beta`.)');
1216
1247
  process.exit(1);
1217
1248
  }
@@ -1239,36 +1270,42 @@ async function release(cwd: string, flags: Record<string, string>, positionals:
1239
1270
  }
1240
1271
  const stampCfg = serverUrl ? { ...cfg, loader: 'server' as const, serverUrl } : cfg;
1241
1272
 
1242
- // Build number: explicit flag → APPWRAP_BUILD_NUMBER (which the CLI's stamping already honors and
1243
- // wins over the derived default). In CI the workflow sets APPWRAP_BUILD_NUMBER itself.
1244
- const env = { ...process.env };
1245
- if (flags['build-number']) {
1246
- if (!/^\d+$/.test(flags['build-number'])) {
1247
- console.error(`✖ --build-number must be a positive integer (got "${flags['build-number']}").`);
1248
- process.exit(1);
1249
- }
1250
- env.APPWRAP_BUILD_NUMBER = flags['build-number'];
1273
+ // Build number: explicit flag → APPWRAP_BUILD_NUMBER. Stamping (sync → stampIOSDisplayName →
1274
+ // buildNumberOf) reads process.env.APPWRAP_BUILD_NUMBER, and sync() runs BEFORE fastlane archives
1275
+ // the (already-stamped) Info.plist — so the flag must land on process.env *here*, before sync(),
1276
+ // not only on the child env. In CI the workflow sets APPWRAP_BUILD_NUMBER itself (env path).
1277
+ try {
1278
+ applyBuildNumberFlag(flags['build-number']);
1279
+ } catch (e) {
1280
+ console.error(`✖ ${(e as Error).message}`);
1281
+ process.exit(1);
1251
1282
  }
1283
+ const env = { ...process.env };
1284
+ // `submit ios --submit-for-review` → also submit the binary for App Store review (the lane defaults
1285
+ // to a safe binary-only promote). The flag is boolean (presence = '' via parseArgs).
1286
+ if (submit && 'submit-for-review' in flags) env.APPWRAP_SUBMIT_FOR_REVIEW = 'true';
1252
1287
 
1253
1288
  // Re-stamp config + copy the latest PWA into native/ so the lane archives current sources. (The lane
1254
1289
  // also runs `ns prepare ios --release`; sync here makes the wrapper config/PWA authoritative first.)
1255
1290
  await sync(cwd, flags);
1256
1291
  if (serverUrl) {
1257
1292
  stampShellConfig(outDir, stampCfg);
1258
- console.log(`✓ Release loader → ${serverUrl}${flags.env ? ` (env: ${flags.env})` : ''}`);
1293
+ console.log(`✓ ${submit ? 'Submit' : 'Release'} loader → ${serverUrl}${flags.env ? ` (env: ${flags.env})` : ''}`);
1259
1294
  }
1260
1295
 
1261
- console.log(`▶ fastlane ios beta (cwd: ${outDir}/fastlane → native/)${env.APPWRAP_BUILD_NUMBER ? ` build #${env.APPWRAP_BUILD_NUMBER}` : ''}`);
1296
+ console.log(`▶ fastlane ios ${lane} (cwd: ${outDir}/fastlane → native/)${env.APPWRAP_BUILD_NUMBER ? ` build #${env.APPWRAP_BUILD_NUMBER}` : ''}`);
1262
1297
  try {
1263
- execFileSync('fastlane', ['ios', 'beta'], { cwd: outDir, stdio: 'inherit', env });
1298
+ execFileSync('fastlane', ['ios', lane], { cwd: outDir, stdio: 'inherit', env });
1264
1299
  } catch {
1265
- console.error('\n✖ TestFlight release failed. Common causes:\n' +
1300
+ console.error(`\n✖ ${submit ? 'App Store submit' : 'TestFlight release'} failed. Common causes:\n` +
1266
1301
  ' • Missing ASC/match env: ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_P8 (base64), MATCH_GIT_URL, MATCH_PASSWORD.\n' +
1267
1302
  ' • Certs/profiles not seeded — run `fastlane match appstore` once against MATCH_GIT_URL.\n' +
1268
1303
  ' • CFBundleVersion already used for this marketing version → pass a higher --build-number.');
1269
1304
  process.exit(1);
1270
1305
  }
1271
- console.log('✓ Uploaded to TestFlight (App Store Connect processing — check the build list / wait for the email).');
1306
+ console.log(submit
1307
+ ? `✓ Binary promoted to the App Store (production)${env.APPWRAP_SUBMIT_FOR_REVIEW === 'true' ? ' and submitted for review' : ' — submit for review in App Store Connect (or pass --submit-for-review)'}.`
1308
+ : '✓ Uploaded to TestFlight (App Store Connect processing — check the build list / wait for the email).');
1272
1309
  }
1273
1310
 
1274
1311
  interface AppleTeam { teamId: string; name: string; email?: string; paid: boolean }
@@ -1808,16 +1845,20 @@ async function main(): Promise<void> {
1808
1845
  await deploy(cwd, flags, positionals);
1809
1846
  break;
1810
1847
  case 'release':
1811
- await release(cwd, flags, positionals);
1848
+ await release(cwd, flags, positionals, 'beta');
1849
+ break;
1850
+ case 'submit':
1851
+ await release(cwd, flags, positionals, 'release');
1812
1852
  break;
1813
1853
  case 'logs':
1814
1854
  await logs(cwd, flags, positionals);
1815
1855
  break;
1816
1856
  default:
1817
- console.log('Usage: appwrap <init|sync|dev|build|deploy|release|logs> [--config <path>] [--out native]\n' +
1857
+ console.log('Usage: appwrap <init|sync|dev|build|deploy|release|submit|logs> [--config <path>] [--out native]\n' +
1818
1858
  ' config: appwrap.config.ts (preferred) → .js → appwrap.json\n' +
1819
1859
  ' build <ios|android> [--release] [--aab] deploy ios [--device <id|name>] [--no-launch]\n' +
1820
1860
  ' release ios [--server-url <url>] [--env <name>] [--build-number <n>] (build+sign+upload to TestFlight)\n' +
1861
+ ' submit ios [--build-number <n>] [--submit-for-review] (promote the binary to the App Store; metadata stays in ASC)\n' +
1821
1862
  ' logs ios [--once] [--native] dev [--url <url> | --port <p>]');
1822
1863
  process.exit(command ? 1 : 0);
1823
1864
  }
package/src/config.ts CHANGED
@@ -34,6 +34,11 @@ export interface AppwrapConfig {
34
34
  * axis; `any` (default) leaves rotation free (sans upside-down on iOS). Stamped into iOS
35
35
  * `UISupportedInterfaceOrientations` (+ `~ipad`) and Android `android:screenOrientation`. */
36
36
  orientation?: 'portrait' | 'landscape' | 'any';
37
+ /** iOS targeted device family. `'iphone'` builds an iPhone-only app (TARGETED_DEVICE_FAMILY=1 →
38
+ * UIDeviceFamily=[1]); `'universal'` (default — NativeScript's behavior) targets iPhone + iPad,
39
+ * which makes the App Store require a full iPad screenshot set. Set `'iphone'` for a phone-only
40
+ * app to avoid that requirement. iOS-only; no-op on Android. */
41
+ targetedDevices?: 'iphone' | 'universal';
37
42
  /** Android only (experimental). When true, the WebView draws genuinely edge-to-edge UNDER the
38
43
  * transparent system bars (NS `androidOverflowEdge='dont-apply'`) and the real safe-area insets
39
44
  * are injected as `--saie-*` CSS vars + native `env(safe-area-inset-*)`, so a multi-theme PWA
package/src/derive.ts CHANGED
@@ -97,6 +97,26 @@ export function resolveBuildNumber(
97
97
  return deriveBuild(cfg.version);
98
98
  }
99
99
 
100
+ /**
101
+ * Apply a `--build-number` CLI flag to the env so stamping picks it up. The stamping chain
102
+ * (sync → stampIOSDisplayName/gradle → buildNumberOf → resolveBuildNumber) reads
103
+ * `APPWRAP_BUILD_NUMBER` off the env, and stamping runs BEFORE the native build/archive — so the
104
+ * flag must land on the env *before* sync(), not only on a child process env copy.
105
+ * Returns the validated build-number string (set on `env`), or undefined if no flag was given.
106
+ * Throws on a non-positive-integer flag so the caller can print + exit.
107
+ */
108
+ export function applyBuildNumberFlag(
109
+ flag: string | undefined,
110
+ env: { APPWRAP_BUILD_NUMBER?: string } = process.env
111
+ ): string | undefined {
112
+ if (flag == null || flag === '') return undefined;
113
+ if (!/^\d+$/.test(flag)) {
114
+ throw new Error(`--build-number must be a positive integer (got "${flag}").`);
115
+ }
116
+ env.APPWRAP_BUILD_NUMBER = flag;
117
+ return flag;
118
+ }
119
+
100
120
  /**
101
121
  * Collapse a PWA web-manifest `orientation` value to our three states. The manifest spec allows
102
122
  * `portrait`/`landscape` plus the `-primary`/`-secondary`/`*-up`/`natural` variants — we don't model
@@ -36,44 +36,54 @@ def appwrap_prepare_ios
36
36
  end
37
37
  end
38
38
 
39
- platform :ios do
40
- desc 'Build the NativeScript wrapper and upload to TestFlight'
41
- lane :beta do
42
- setup_ci # on CI: provisions a temporary unlocked keychain so codesign can use the match cert
39
+ # Shared archive-and-sign sequence for BOTH the TestFlight (:beta) and App Store (:release) lanes.
40
+ # Returns the ASC API-key object so each lane can reuse it for its final upload step — the lanes
41
+ # differ ONLY in that final step (upload_to_testflight vs upload_to_app_store). Keep the recipe here,
42
+ # not duplicated per-lane.
43
+ def appwrap_archive_and_sign_ios
44
+ setup_ci # on CI: provisions a temporary unlocked keychain so codesign can use the match cert
43
45
 
44
- api_key = app_store_connect_api_key(
45
- key_id: ENV['ASC_KEY_ID'],
46
- issuer_id: ENV['ASC_ISSUER_ID'],
47
- key_content: ENV['ASC_KEY_P8'],
48
- is_key_content_base64: true
49
- )
46
+ api_key = app_store_connect_api_key(
47
+ key_id: ENV['ASC_KEY_ID'],
48
+ issuer_id: ENV['ASC_ISSUER_ID'],
49
+ key_content: ENV['ASC_KEY_P8'],
50
+ is_key_content_base64: true
51
+ )
50
52
 
51
- # readonly: certs/profiles must be seeded once via `fastlane match appstore` (see workflow header).
52
- match(type: 'appstore', readonly: true, app_identifier: APP_ID,
53
- git_url: ENV['MATCH_GIT_URL'], api_key: api_key)
53
+ # readonly: certs/profiles must be seeded once via `fastlane match appstore` (see workflow header).
54
+ match(type: 'appstore', readonly: true, app_identifier: APP_ID,
55
+ git_url: ENV['MATCH_GIT_URL'], api_key: api_key)
54
56
 
55
- # NativeScript prepares the Xcode project inside the wrapper dir (native/ = parent of fastlane/).
56
- # Wrapped to sanitize the gem env so the system `pod` can find CocoaPods (see appwrap_prepare_ios).
57
- appwrap_prepare_ios
57
+ # NativeScript prepares the Xcode project inside the wrapper dir (native/ = parent of fastlane/).
58
+ # Wrapped to sanitize the gem env so the system `pod` can find CocoaPods (see appwrap_prepare_ios).
59
+ appwrap_prepare_ios
58
60
 
59
- # NS defaults the project to automatic signing → archive can't find a profile. Pin manual signing
60
- # to the match-provided App Store profile (name exported by match into ENV).
61
- profile = ENV["sigh_#{APP_ID}_appstore_profile-name"] || "match AppStore #{APP_ID}"
62
- update_code_signing_settings(
63
- use_automatic_signing: false,
64
- path: 'platforms/ios/native.xcodeproj',
65
- team_id: TEAM_ID,
66
- bundle_identifier: APP_ID,
67
- code_sign_identity: 'Apple Distribution',
68
- profile_name: profile
69
- )
61
+ # NS defaults the project to automatic signing → archive can't find a profile. Pin manual signing
62
+ # to the match-provided App Store profile (name exported by match into ENV).
63
+ profile = ENV["sigh_#{APP_ID}_appstore_profile-name"] || "match AppStore #{APP_ID}"
64
+ update_code_signing_settings(
65
+ use_automatic_signing: false,
66
+ path: 'platforms/ios/native.xcodeproj',
67
+ team_id: TEAM_ID,
68
+ bundle_identifier: APP_ID,
69
+ code_sign_identity: 'Apple Distribution',
70
+ profile_name: profile
71
+ )
70
72
 
71
- build_app(
72
- workspace: 'platforms/ios/native.xcworkspace',
73
- scheme: 'native',
74
- export_method: 'app-store',
75
- export_options: { provisioningProfiles: { APP_ID => profile } }
76
- )
73
+ build_app(
74
+ workspace: 'platforms/ios/native.xcworkspace',
75
+ scheme: 'native',
76
+ export_method: 'app-store',
77
+ export_options: { provisioningProfiles: { APP_ID => profile } }
78
+ )
79
+
80
+ api_key
81
+ end
82
+
83
+ platform :ios do
84
+ desc 'Build the NativeScript wrapper and upload to TestFlight'
85
+ lane :beta do
86
+ api_key = appwrap_archive_and_sign_ios
77
87
 
78
88
  # Wait for App Store Connect to finish processing and FAIL if it doesn't — Apple can accept the
79
89
  # upload yet silently drop the build (e.g. an out-of-range CFBundleVersion). skip:true hides that.
@@ -85,6 +95,26 @@ platform :ios do
85
95
  tf_wait_timeout = tf_wait_env.to_i > 0 ? tf_wait_env.to_i : 3600
86
96
  upload_to_testflight(api_key: api_key, skip_waiting_for_build_processing: false, wait_processing_timeout_duration: tf_wait_timeout)
87
97
  end
98
+
99
+ desc 'Build the NativeScript wrapper and promote the binary to the App Store (production).'
100
+ lane :release do
101
+ api_key = appwrap_archive_and_sign_ios
102
+
103
+ # Binary-only promote: the listing/metadata/screenshots/IAP stay in App Store Connect — this lane
104
+ # only ships the signed binary. SAFE by default: no review submit, no automatic release, headless
105
+ # (force: true → no HTML preview). Set APPWRAP_SUBMIT_FOR_REVIEW=true to also submit for review.
106
+ # Guard BOTH unset (nil) and blank env the same way as APPWRAP_TF_WAIT_TIMEOUT above.
107
+ submit_for_review = ENV['APPWRAP_SUBMIT_FOR_REVIEW'].to_s.strip.downcase == 'true'
108
+ upload_to_app_store(
109
+ api_key: api_key,
110
+ submit_for_review: submit_for_review,
111
+ automatic_release: false,
112
+ skip_metadata: true,
113
+ skip_screenshots: true,
114
+ precheck_include_in_app_purchases: false,
115
+ force: true
116
+ )
117
+ end
88
118
  end
89
119
 
90
120
  platform :android do