@livx.cc/appwrap 0.28.2 → 0.29.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.28.2",
3
+ "version": "0.29.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
@@ -24,6 +24,7 @@ import {
24
24
  androidScreenOrientation,
25
25
  iosOrientations,
26
26
  mergeManifest,
27
+ resolveBuildNumber,
27
28
  stampAndroidOrientation,
28
29
  stampAndroidQueries,
29
30
  stampPlistBackgroundTasks,
@@ -45,31 +46,15 @@ const execErrText = (e: unknown): string => {
45
46
  return `${err.stdout ?? ''}${err.stderr ?? ''}`;
46
47
  };
47
48
 
48
- /** Marketing version → a monotonic integer build (0.2.1 → 201; 1.4.12 → 10412). Stable & increasing
49
- * across semver bumps so store re-uploads are always accepted without a manual bump. */
50
- function deriveBuild(version: string): number {
51
- const [maj = 0, min = 0, patch = 0] = version.split('.').map((n) => parseInt(n, 10) || 0);
52
- return maj * 10000 + min * 100 + patch;
53
- }
54
-
55
49
  /**
56
- * Resolved monotonic build number (iOS CFBundleVersion / Android versionCode).
57
- * Precedence: `APPWRAP_BUILD_NUMBER` env (CI run #) > explicit `cfg.buildNumber` > derived from version.
58
- * The env override means CI gets a monotonic, collision-free build number for free — no per-app config
59
- * plumbing — which is the whole point: the derived default is CONSTANT per version, so repeat uploads
60
- * of the same marketing version would 409 ("build already exists") without it.
50
+ * Resolved monotonic build number (iOS CFBundleVersion / Android versionCode). Thin env wrapper over
51
+ * the pure `resolveBuildNumber` (in derive.ts, where it unit-tests). Precedence: `APPWRAP_BUILD_NUMBER`
52
+ * env (CI run #) > explicit numeric `cfg.buildNumber` > named strategy ('timestamp'|'epoch') > derived
53
+ * from version. The env override gives CI a monotonic, collision-free build for free; the derived
54
+ * default is CONSTANT per version, so repeat uploads of one marketing version would 409 without it.
61
55
  */
62
56
  function buildNumberOf(cfg: AppwrapConfig): number {
63
- const env = process.env.APPWRAP_BUILD_NUMBER;
64
- if (env != null && env !== '') {
65
- const n = parseInt(env, 10);
66
- if (!Number.isNaN(n)) return n;
67
- }
68
- if (cfg.buildNumber != null) {
69
- const n = parseInt(String(cfg.buildNumber), 10);
70
- if (!Number.isNaN(n)) return n;
71
- }
72
- return deriveBuild(cfg.version);
57
+ return resolveBuildNumber(cfg, process.env.APPWRAP_BUILD_NUMBER);
73
58
  }
74
59
 
75
60
  const IOS_PERMISSION_KEYS: Record<string, string[]> = {
package/src/config.ts CHANGED
@@ -113,8 +113,12 @@ export interface AppwrapConfig {
113
113
  >;
114
114
  /** Monotonic build identifier. Stores reject a re-upload unless this is HIGHER than the last:
115
115
  * iOS `CFBundleVersion`, Android `versionCode` (the marketing `version` stays the user-facing
116
- * string). Default: an integer derived from `version` (0.2.1 → 201). Set explicitly from a CI
117
- * run number for fleet builds of the same marketing version. */
116
+ * string). Default: an integer derived from `version` (0.2.1 → 201). Set an explicit number from a
117
+ * CI run for fleet builds of one marketing version, OR a named strategy string (resolved by the
118
+ * framework so it can't drift across branches):
119
+ * - `'timestamp'` — YYMMDDHHMM UTC (e.g. 2606221405). iOS-ONLY: exceeds Android's versionCode cap.
120
+ * - `'epoch'` — unix seconds. Android-safe.
121
+ * (`APPWRAP_BUILD_NUMBER` env always wins; an unknown string falls back to the derived default.) */
118
122
  buildNumber?: string | number;
119
123
  /** iOS export-compliance. `ITSAppUsesNonExemptEncryption` — stamped `false` by default (skips the
120
124
  * per-upload prompt). Set `true` only if the app uses non-exempt encryption. */
package/src/derive.ts CHANGED
@@ -47,6 +47,56 @@ export function mergeManifest<
47
47
  return cfg;
48
48
  }
49
49
 
50
+ /** Marketing version → a monotonic integer build (0.2.1 → 201; 1.4.12 → 10412). Stable & increasing
51
+ * across semver bumps so store re-uploads are always accepted without a manual bump. */
52
+ export function deriveBuild(version: string): number {
53
+ const [maj = 0, min = 0, patch = 0] = version.split('.').map((n) => parseInt(n, 10) || 0);
54
+ return maj * 10000 + min * 100 + patch;
55
+ }
56
+
57
+ /** Named build-number strategies. Live ONCE here so apps opt into a name instead of copying the
58
+ * stamping logic into their config (which drifts across branches). All return a single integer.
59
+ * - `timestamp`: human-readable YYMMDDHHMM (UTC), e.g. 2026-06-22 14:05 → 2606221405. ≈2.6e9, ≤ the
60
+ * iOS CFBundleVersion UInt32 cap (4294967295) until ~2042 — but it EXCEEDS Android versionCode's
61
+ * 2.1e9 cap, so this strategy is iOS-ONLY.
62
+ * - `epoch`: unix seconds (Math.floor(Date.now()/1000)), ≈1.78e9 — Android-safe. */
63
+ const BUILD_STRATEGIES: Record<string, (now: Date) => number> = {
64
+ timestamp: (now) => {
65
+ const p = (n: number) => String(n).padStart(2, '0');
66
+ const yy = now.getUTCFullYear() % 100;
67
+ return Number(
68
+ `${p(yy)}${p(now.getUTCMonth() + 1)}${p(now.getUTCDate())}${p(now.getUTCHours())}${p(now.getUTCMinutes())}`
69
+ );
70
+ },
71
+ epoch: (now) => Math.floor(now.getTime() / 1000),
72
+ };
73
+
74
+ /**
75
+ * Resolve the monotonic build number (iOS CFBundleVersion / Android versionCode). Pure: the CLI
76
+ * passes the `APPWRAP_BUILD_NUMBER` env value in (no process.env read here, so it unit-tests).
77
+ * Precedence: env (CI run #) > explicit numeric `buildNumber` > named strategy > derived from version.
78
+ * An unrecognized strategy string falls back to `deriveBuild` (never crashes).
79
+ */
80
+ export function resolveBuildNumber(
81
+ cfg: { version: string; buildNumber?: string | number },
82
+ envBuildNumber?: string,
83
+ now: Date = new Date()
84
+ ): number {
85
+ if (envBuildNumber != null && envBuildNumber !== '') {
86
+ const n = parseInt(envBuildNumber, 10);
87
+ if (!Number.isNaN(n)) return n;
88
+ }
89
+ if (cfg.buildNumber != null) {
90
+ // A numeric value (number or numeric string) is the literal build number.
91
+ const n = parseInt(String(cfg.buildNumber), 10);
92
+ if (!Number.isNaN(n)) return n;
93
+ // Non-numeric → a strategy NAME. Map it, or fall back to deriveBuild for an unknown string.
94
+ const strategy = BUILD_STRATEGIES[String(cfg.buildNumber).trim()];
95
+ if (strategy) return strategy(now);
96
+ }
97
+ return deriveBuild(cfg.version);
98
+ }
99
+
50
100
  /**
51
101
  * Collapse a PWA web-manifest `orientation` value to our three states. The manifest spec allows
52
102
  * `portrait`/`landscape` plus the `-primary`/`-secondary`/`*-up`/`natural` variants — we don't model
@@ -46,7 +46,13 @@ platform :ios do
46
46
 
47
47
  # Wait for App Store Connect to finish processing and FAIL if it doesn't — Apple can accept the
48
48
  # upload yet silently drop the build (e.g. an out-of-range CFBundleVersion). skip:true hides that.
49
- upload_to_testflight(api_key: api_key, skip_waiting_for_build_processing: false, wait_processing_timeout_duration: 1800)
49
+ # APPWRAP_TF_WAIT_TIMEOUT (seconds) tunes the wait: real builds can exceed 30 min under Apple
50
+ # processing load and crash the lane (BuildWatcher exceeded) — default to 1h, raise it if needed.
51
+ # Guard BOTH unset (nil) and empty/blank env: nil.to_i and "".to_i are 0, which would crash the
52
+ # lane instantly (BuildWatcher exceeded '0'). Only a positive integer overrides the 3600 default.
53
+ tf_wait_env = ENV['APPWRAP_TF_WAIT_TIMEOUT'].to_s.strip
54
+ tf_wait_timeout = tf_wait_env.to_i > 0 ? tf_wait_env.to_i : 3600
55
+ upload_to_testflight(api_key: api_key, skip_waiting_for_build_processing: false, wait_processing_timeout_duration: tf_wait_timeout)
50
56
  end
51
57
  end
52
58