@livx.cc/appwrap 0.39.13 → 0.39.14

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.ts +122 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livx.cc/appwrap",
3
- "version": "0.39.13",
3
+ "version": "0.39.14",
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
@@ -1714,14 +1714,16 @@ function buildFingerprint(cwd: string, cfg: { pwaDist?: string }, _outDir: strin
1714
1714
  return h.toString(36);
1715
1715
  }
1716
1716
 
1717
- const BUILD_CACHE_FILE = '.appwrap-build-cache.json';
1718
- interface BuildCache { fingerprint: string; ipaPath: string; builtAt: string }
1717
+ // Per-platform build cache (iOS .ipa / Android .apk) — separate files so the two don't clobber each
1718
+ // other's fingerprint (that's what enables the build-skip on BOTH platforms).
1719
+ const buildCacheFile = (platform: string) => `.appwrap-build-cache-${platform}.json`;
1720
+ interface BuildCache { fingerprint: string; artifactPath: string; builtAt: string }
1719
1721
 
1720
- function readBuildCache(outDir: string): BuildCache | null {
1721
- try { return JSON.parse(readFileSync(join(outDir, BUILD_CACHE_FILE), 'utf8')); } catch { return null; }
1722
+ function readBuildCache(outDir: string, platform: string): BuildCache | null {
1723
+ try { return JSON.parse(readFileSync(join(outDir, buildCacheFile(platform)), 'utf8')); } catch { return null; }
1722
1724
  }
1723
- function writeBuildCache(outDir: string, cache: BuildCache): void {
1724
- try { writeFileSync(join(outDir, BUILD_CACHE_FILE), JSON.stringify(cache, null, 2)); } catch { /* non-fatal */ }
1725
+ function writeBuildCache(outDir: string, platform: string, cache: BuildCache): void {
1726
+ try { writeFileSync(join(outDir, buildCacheFile(platform)), JSON.stringify(cache, null, 2)); } catch { /* non-fatal */ }
1725
1727
  }
1726
1728
 
1727
1729
  /** Discover usable physical iOS devices via devicectl (USB + network). Excludes 'unavailable'
@@ -1776,6 +1778,40 @@ function pickDevice(devices: DeviceInfo[], explicitId?: string): DeviceInfo {
1776
1778
  * distribution signing) — for testing on your own device. Run the PWA build first (or via the script).
1777
1779
  * iOS has a bespoke Debug-IPA path (below); Android delegates to `run` (NativeScript builds + installs
1778
1780
  * + launches), so the `deploy <platform>` surface is symmetric across both. */
1781
+ /** The project's web-build command: explicit `webBuild` in config, else `bun run build` if the
1782
+ * project's package.json has a "build" script. null when there's nothing to run. */
1783
+ function detectWebBuildCmd(cwd: string, cfg: AppwrapConfig): string[] | null {
1784
+ const explicit = (cfg as { webBuild?: string }).webBuild;
1785
+ if (explicit) return explicit.trim().split(/\s+/);
1786
+ try {
1787
+ const pj = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
1788
+ if (pj.scripts?.build) return ['bun', 'run', 'build'];
1789
+ } catch { /* no package.json */ }
1790
+ return null;
1791
+ }
1792
+
1793
+ /** Build the web bundle before a device deploy, but ONLY for a bundled loader — a `loader:'server'`
1794
+ * app loads the live serverUrl, so its dist is unused (building it would be wasteful + misleading).
1795
+ * Prints exactly what it's doing either way. `--no-web-build` skips it (ship the current dist as-is,
1796
+ * e.g. to hit the native build-skip fast-path when only the wrapper changed). */
1797
+ function buildWebIfBundled(cwd: string, cfg: AppwrapConfig, flags: Record<string, string>): void {
1798
+ if ((cfg.loader ?? 'app') === 'server') {
1799
+ console.log('ℹ loader:server — NOT building the web (the app loads serverUrl live; the bundle is unused).');
1800
+ return;
1801
+ }
1802
+ if ('no-web-build' in flags) {
1803
+ console.log('ℹ --no-web-build — shipping the CURRENT dist/ as-is (web NOT rebuilt).');
1804
+ return;
1805
+ }
1806
+ const cmd = detectWebBuildCmd(cwd, cfg);
1807
+ if (!cmd) {
1808
+ console.warn('⚠ Bundled loader but no web-build command (no package.json "build" script / `webBuild` config). Shipping the CURRENT dist/ — it may be STALE.');
1809
+ return;
1810
+ }
1811
+ console.log(`▶ ${cmd.join(' ')} (bundled loader → fresh web bundle so dist isn't stale)`);
1812
+ execFileSync(cmd[0], cmd.slice(1), { cwd, stdio: 'inherit' });
1813
+ }
1814
+
1779
1815
  /** Resolve adb: $ANDROID_HOME/$ANDROID_SDK_ROOT platform-tools, else `adb` on PATH. */
1780
1816
  function androidAdb(): string {
1781
1817
  const home = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
@@ -1833,13 +1869,25 @@ async function deployAndroid(cwd: string, flags: Record<string, string>): Promis
1833
1869
  const adb = androidAdb();
1834
1870
  const device = pickAndroidDevice(adb, flags.device || undefined); // fail fast before the build
1835
1871
 
1872
+ buildWebIfBundled(cwd, cfg, flags); // bundled → fresh web bundle; server → skip (both printed)
1836
1873
  await sync(cwd, flags); // re-stamp config + copy latest PWA dist
1837
1874
  stampShellConfig(outDir, { ...cfg, debug: true }); // debug: keep-awake + WebView inspector (parity with deploy ios)
1838
1875
 
1839
- console.log('▶ ns build android (debug)');
1840
- runNs(outDir, ['build', 'android']); // bun-installs deps if needed, then gradle build
1841
1876
  const apk = join(outDir, 'platforms/android/app/build/outputs/apk/debug/app-debug.apk');
1842
- if (!existsSync(apk)) { console.error(`✖ No APK produced at ${apk}`); process.exit(1); }
1877
+ // Fingerprint build-skip (parity with deploy ios): skip the gradle build when dist + config are
1878
+ // unchanged since the last build. --force/-f always rebuilds. (Rebuilding the web above usually
1879
+ // bumps the fingerprint; pair with --no-web-build to actually hit this fast-path.)
1880
+ const force = 'force' in flags || 'f' in flags;
1881
+ const fp = buildFingerprint(cwd, cfg, outDir);
1882
+ const cache = readBuildCache(outDir, 'android');
1883
+ if (!force && existsSync(apk) && cache?.fingerprint === fp && cache?.artifactPath === apk) {
1884
+ console.log(`⚡ Skipping build — inputs unchanged since last build (${apk.split('/').pop()})`);
1885
+ } else {
1886
+ console.log(`▶ ns build android (debug)${force ? ' [--force]' : ''}`);
1887
+ runNs(outDir, ['build', 'android']); // bun-installs deps if needed, then gradle build
1888
+ if (!existsSync(apk)) { console.error(`✖ No APK produced at ${apk}`); process.exit(1); }
1889
+ writeBuildCache(outDir, 'android', { fingerprint: fp, artifactPath: apk, builtAt: new Date().toISOString() });
1890
+ }
1843
1891
 
1844
1892
  console.log(`▶ installing → ${device}`);
1845
1893
  try {
@@ -1883,6 +1931,7 @@ async function deploy(cwd: string, flags: Record<string, string>, positionals: s
1883
1931
  // Pick the device up front so we fail fast before a long build if nothing's connected.
1884
1932
  const device = pickDevice(listIosDevices(), flags.device || undefined);
1885
1933
 
1934
+ buildWebIfBundled(cwd, cfg, flags); // bundled → fresh web bundle (no stale dist); server → skip (printed)
1886
1935
  await sync(cwd, flags); // re-stamp config + copy latest PWA dist (+ vendor backend assets)
1887
1936
  // Dev deploy → debug mode: keep-awake + WebView inspector for continuous troubleshooting.
1888
1937
  stampShellConfig(outDir, { ...cfg, debug: true });
@@ -1896,11 +1945,11 @@ async function deploy(cwd: string, flags: Record<string, string>, positionals: s
1896
1945
  const resume = 'resume' in flags || 'r' in flags;
1897
1946
  const force = 'force' in flags || 'f' in flags;
1898
1947
  const fp = buildFingerprint(cwd, cfg, outDir);
1899
- const cache = readBuildCache(outDir);
1948
+ const cache = readBuildCache(outDir, 'ios');
1900
1949
  const existingIpa = existsSync(ipaDir)
1901
1950
  ? readdirSync(ipaDir).find((f) => f.endsWith('.ipa'))
1902
1951
  : undefined;
1903
- const fingerprintMatch = !force && existingIpa && cache?.fingerprint === fp && cache?.ipaPath === join(ipaDir, existingIpa);
1952
+ const fingerprintMatch = !force && existingIpa && cache?.fingerprint === fp && cache?.artifactPath === join(ipaDir, existingIpa);
1904
1953
  // --resume also accepts a missing cache file (e.g. after a manual Xcode build or first run),
1905
1954
  // but ONLY when the fingerprint matches what's currently on disk — never skips a needed build.
1906
1955
  const noCache = existingIpa && !cache;
@@ -1932,7 +1981,7 @@ async function deploy(cwd: string, flags: Record<string, string>, positionals: s
1932
1981
  const ipa = builtIpa;
1933
1982
  if (!ipa) { console.error(`✖ No .ipa produced in ${ipaDir}`); process.exit(1); }
1934
1983
  const ipaPath = join(ipaDir, ipa);
1935
- if (!canSkipBuild) writeBuildCache(outDir, { fingerprint: fp, ipaPath, builtAt: new Date().toISOString() });
1984
+ if (!canSkipBuild) writeBuildCache(outDir, 'ios', { fingerprint: fp, artifactPath: ipaPath, builtAt: new Date().toISOString() });
1936
1985
 
1937
1986
  console.log(`▶ installing ${ipa} → ${device.name} [${device.transport}]`);
1938
1987
  let installedViaUsbmux = false;
@@ -2061,12 +2110,28 @@ function libimobiledeviceUdid(): { udid: string; network: boolean } | null {
2061
2110
  * Headless-friendly: redirect to a file and read it. */
2062
2111
  async function logs(cwd: string, flags: Record<string, string>, positionals: string[]): Promise<void> {
2063
2112
  const platform = positionals[0] ?? 'ios';
2064
- if (platform !== 'ios') {
2065
- console.error('Usage: appwrap logs ios [--once] [--native] [--device <id|name>]');
2113
+ if (platform !== 'ios' && platform !== 'android') {
2114
+ console.error('Usage: appwrap logs <ios|android> [--once] [--native] [--device <id|name>]');
2066
2115
  process.exit(1);
2067
2116
  }
2068
2117
  const cfg = await loadConfig(cwd, flags);
2069
2118
 
2119
+ // ── Android: adb logcat — WebView console (chromium tag) by default; --native = full app logcat ──
2120
+ if (platform === 'android') {
2121
+ const adb = androidAdb();
2122
+ const device = pickAndroidDevice(adb, flags.device || undefined);
2123
+ const once = 'once' in flags;
2124
+ if ('native' in flags) {
2125
+ const pid = (() => { try { return execFileSync(adb, ['-s', device, 'shell', 'pidof', cfg.id], { encoding: 'utf8' }).trim().split(/\s+/)[0]; } catch { return ''; } })();
2126
+ console.log(`▶ logcat for ${cfg.id}${pid ? ` (pid ${pid})` : ' (not running — full stream)'}${once ? ' [once]' : ''} — Ctrl-C to stop.`);
2127
+ try { execFileSync(adb, ['-s', device, 'logcat', ...(once ? ['-d'] : []), ...(pid ? ['--pid', pid] : [])], { stdio: 'inherit' }); } catch { process.exit(1); }
2128
+ return;
2129
+ }
2130
+ console.log(`▶ WebView console (chromium) for ${cfg.id}${once ? ' [once]' : ''} — Ctrl-C to stop. (--native for full app logcat)`);
2131
+ try { execFileSync(adb, ['-s', device, 'logcat', ...(once ? ['-d'] : []), '-s', 'chromium:I'], { stdio: 'inherit' }); } catch { process.exit(1); }
2132
+ return;
2133
+ }
2134
+
2070
2135
  if ('native' in flags) {
2071
2136
  const li = libimobiledeviceUdid();
2072
2137
  if (!li) {
@@ -2116,6 +2181,40 @@ async function logs(cwd: string, flags: Record<string, string>, positionals: str
2116
2181
 
2117
2182
  /** CLI dispatch. Guarded by `import.meta.main` so importing this module (e.g. for the `AppwrapConfig`
2118
2183
  * type via the package entry) doesn't run a command. */
2184
+ /** `appwrap debug <ios|android>` — point at the WebView inspector for the (debug-built) app, then
2185
+ * stream its console. Debug builds enable the inspector (deploy installs one); this just opens the
2186
+ * door + tails logs — no rebuild. Android: adb-forwards the WebView devtools socket → chrome://inspect.
2187
+ * iOS: prints the Safari Web Inspector path. Both then fall through to `logs` for the live console. */
2188
+ async function debug(cwd: string, flags: Record<string, string>, positionals: string[]): Promise<void> {
2189
+ const platform = positionals[0];
2190
+ if (platform !== 'ios' && platform !== 'android') {
2191
+ console.error('Usage: appwrap debug <ios|android> [--device <id|name>]');
2192
+ process.exit(1);
2193
+ }
2194
+ const cfg = await loadConfig(cwd, flags);
2195
+ if (platform === 'android') {
2196
+ const adb = androidAdb();
2197
+ const device = pickAndroidDevice(adb, flags.device || undefined);
2198
+ const pid = (() => { try { return execFileSync(adb, ['-s', device, 'shell', 'pidof', cfg.id], { encoding: 'utf8' }).trim().split(/\s+/)[0]; } catch { return ''; } })();
2199
+ if (pid) {
2200
+ try {
2201
+ execFileSync(adb, ['-s', device, 'forward', 'tcp:9222', `localabstract:webview_devtools_remote_${pid}`], { stdio: 'pipe' });
2202
+ console.log('✓ WebView devtools forwarded → open chrome://inspect (or http://localhost:9222) in desktop Chrome to inspect the page.');
2203
+ } catch {
2204
+ console.log('⚠ Could not forward the devtools socket — open chrome://inspect and look for the device there.');
2205
+ }
2206
+ } else {
2207
+ console.log(`⚠ ${cfg.id} not running — launch it (or \`appwrap deploy android\`), then open chrome://inspect.`);
2208
+ }
2209
+ console.log(' (Needs a DEBUG build — `appwrap deploy android` installs one with the inspector enabled.)\n');
2210
+ } else {
2211
+ console.log('▶ iOS WebView inspector: Safari → Develop → [your iPhone] → [the app]. Enable it first in iOS Settings → Safari → Advanced → Web Inspector.');
2212
+ console.log(' (Needs a DEBUG build — `appwrap deploy ios` installs one.)\n');
2213
+ }
2214
+ // Tail the live console (reuses logs' per-platform streaming).
2215
+ await logs(cwd, flags, positionals);
2216
+ }
2217
+
2119
2218
  async function main(): Promise<void> {
2120
2219
  const { command, flags, positionals } = parseArgs(process.argv.slice(2));
2121
2220
  const cwd = process.cwd();
@@ -2148,14 +2247,20 @@ async function main(): Promise<void> {
2148
2247
  case 'logs':
2149
2248
  await logs(cwd, flags, positionals);
2150
2249
  break;
2250
+ case 'debug':
2251
+ await debug(cwd, flags, positionals);
2252
+ break;
2151
2253
  default:
2152
- console.log('Usage: appwrap <init|sync|dev|run|build|deploy|release|submit|logs> [--config <path>] [--out native]\n' +
2254
+ console.log('Usage: appwrap <init|sync|dev|run|build|deploy|release|submit|logs|debug> [--config <path>] [--out native]\n' +
2153
2255
  ' config: appwrap.config.ts (preferred) → .js → appwrap.json\n' +
2154
2256
  ' run <ios|android> [--device <id|name>] (compile + boot in a simulator/emulator, live reload)\n' +
2155
- ' build <ios|android> [--release] [--aab] deploy <ios|android> [--device <id|name>] [--no-launch]\n' +
2257
+ ' deploy <ios|android> [--device <id|name>] [--no-launch] [--no-web-build] [-f] (build → install to device → launch; builds web if bundled)\n' +
2258
+ ' build <ios|android> [--release] [--aab] (store artifact)\n' +
2156
2259
  ' release ios [--server-url <url>] [--env <name>] [--build-number <n>] (build+sign+upload to TestFlight)\n' +
2157
2260
  ' submit ios [--build-number <n>] [--submit-for-review] (promote the binary to the App Store; metadata stays in ASC)\n' +
2158
- ' logs ios [--once] [--native] dev [--url <url> | --port <p>]');
2261
+ ' logs <ios|android> [--once] [--native] (stream WebView console; --native = full OS log)\n' +
2262
+ ' debug <ios|android> [--device <id|name>] (open the WebView inspector + stream console)\n' +
2263
+ ' dev [--url <url> | --port <p>]');
2159
2264
  process.exit(command ? 1 : 0);
2160
2265
  }
2161
2266
  }