@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.
- package/package.json +1 -1
- package/src/cli.ts +122 -17
package/package.json
CHANGED
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
|
-
|
|
1718
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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?.
|
|
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
|
-
'
|
|
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]
|
|
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
|
}
|