@hot-updater/android-helper 0.25.4

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/LICENSE ADDED
@@ -0,0 +1,33 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Sungyu Kang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ ADDITIONAL DISCLAIMER
26
+
27
+ The authors of this software shall not be held responsible for any legal issues,
28
+ regulatory violations, policy violations, or disputes arising from the use,
29
+ distribution, or integration of this software.
30
+
31
+ Users are solely responsible for ensuring compliance with all applicable laws,
32
+ regulations, and third-party policies (including but not limited to app store
33
+ policies).
package/dist/index.cjs ADDED
@@ -0,0 +1,626 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+
23
+ //#endregion
24
+ let __hot_updater_cli_tools = require("@hot-updater/cli-tools");
25
+ __hot_updater_cli_tools = __toESM(__hot_updater_cli_tools);
26
+ let __hot_updater_plugin_core = require("@hot-updater/plugin-core");
27
+ __hot_updater_plugin_core = __toESM(__hot_updater_plugin_core);
28
+ let path = require("path");
29
+ path = __toESM(path);
30
+ let execa = require("execa");
31
+ execa = __toESM(execa);
32
+ let fs = require("fs");
33
+ fs = __toESM(fs);
34
+ let node_os = require("node:os");
35
+ node_os = __toESM(node_os);
36
+
37
+ //#region src/utils/createGradleLogger.ts
38
+ const createGradleLogger = () => new __hot_updater_cli_tools.BuildLogger({
39
+ failurePatterns: ["BUILD FAILED"],
40
+ importantLogPatterns: [
41
+ "error:",
42
+ "Error:",
43
+ "ERROR:",
44
+ "> Task :",
45
+ "BUILD FAILED",
46
+ "BUILD SUCCESSFUL",
47
+ "FAILURE: Build failed with an exception.",
48
+ "* What went wrong:",
49
+ "Execution failed for task",
50
+ "Could not resolve",
51
+ "Failed to",
52
+ "Unable to load script",
53
+ "Metro encountered an error",
54
+ "React Native CLI",
55
+ "Android resource compilation failed",
56
+ "Duplicate class",
57
+ "Program type already present",
58
+ "actionable tasks:"
59
+ ],
60
+ progressMapping: [
61
+ [[
62
+ "buildKotlinToolingMetadata",
63
+ "generateAutolinkingNewArchitectureFiles",
64
+ "generateCodegenSchemaFromJavaScript"
65
+ ], 5],
66
+ [[
67
+ "createBundleReleaseJsAndAssets",
68
+ "bundleReleaseJsAndAssets",
69
+ "bundleDebugJsAndAssets"
70
+ ], 15],
71
+ [[
72
+ "generateReleaseResValues",
73
+ "generateReleaseResources",
74
+ "mergeReleaseResources"
75
+ ], 25],
76
+ [[
77
+ "processReleaseResources",
78
+ "processReleaseManifest",
79
+ "parseReleaseLocalResources"
80
+ ], 35],
81
+ [["compileReleaseKotlin", "compileDebugKotlin"], 50],
82
+ [["compileReleaseJavaWithJavac", "compileDebugJavaWithJavac"], 60],
83
+ [[
84
+ "dexBuilderRelease",
85
+ "mergeDexRelease",
86
+ "transformClassesWithDex"
87
+ ], 75],
88
+ [[
89
+ "compressReleaseAssets",
90
+ "optimizeReleaseResources",
91
+ "mergeReleaseAssets"
92
+ ], 85],
93
+ [[
94
+ "packageRelease",
95
+ "packageDebug",
96
+ "assembleRelease",
97
+ "assembleDebug"
98
+ ], 90],
99
+ [["bundleRelease", "bundleDebug"], 95],
100
+ [[/\d+ actionable tasks:/], 100]
101
+ ]
102
+ });
103
+
104
+ //#endregion
105
+ //#region src/builder/runGradle.ts
106
+ /**
107
+ * Clean up gradle error message by removing common noise
108
+ */
109
+ const getCleanedErrorMessage = (error) => {
110
+ const gradleLinesToRemove = [
111
+ "FAILURE: Build failed with an exception.",
112
+ "* Try:",
113
+ "> Run with --stacktrace option to get the stack trace.",
114
+ "> Run with --info or --debug option to get more log output.",
115
+ "> Run with --scan to get full insights.",
116
+ "> Get more help at [undefined](https://help.gradle.org).",
117
+ "> Get more help at https://help.gradle.org.",
118
+ "BUILD FAILED"
119
+ ];
120
+ return error.message.split("\n").filter((line) => !gradleLinesToRemove.some((l) => line.includes(l))).join("\n").trim();
121
+ };
122
+ /**
123
+ * Generate gradle task names with module prefix
124
+ */
125
+ function getTaskNames(moduleName, tasks) {
126
+ return tasks.map((task) => `${moduleName}:${task}`);
127
+ }
128
+ /**
129
+ * Get gradle wrapper command based on platform
130
+ */
131
+ const getGradleWrapper = () => process.platform.startsWith("win") ? "gradlew.bat" : "./gradlew";
132
+ /**
133
+ * Run gradle build with specified tasks and arguments
134
+ */
135
+ async function runGradle({ tasks, args, androidProjectPath, appModuleName }) {
136
+ const gradleArgs = getTaskNames(appModuleName, tasks);
137
+ gradleArgs.push("-x", "lint");
138
+ if (args.extraParams) gradleArgs.push(...args.extraParams);
139
+ __hot_updater_cli_tools.p.log.info(`Run Gradle Settings:
140
+ Project ${androidProjectPath}
141
+ App Moudle ${appModuleName}
142
+ Tasks ${tasks.join(", ")}
143
+ Args ${gradleArgs.join(" ")}
144
+ `);
145
+ if ("port" in args && args.port != null) gradleArgs.push(`-PreactNativeDevServerPort=${args.port}`);
146
+ const logger = createGradleLogger();
147
+ logger.start(`${appModuleName}`);
148
+ try {
149
+ const process$1 = (0, execa.execa)(getGradleWrapper(), gradleArgs, { cwd: androidProjectPath });
150
+ for await (const line of process$1) if (line) logger.processLine(line);
151
+ logger.stop();
152
+ } catch (e) {
153
+ logger.stop("Build failed", false);
154
+ if (e instanceof execa.ExecaError) __hot_updater_cli_tools.p.log.error(getCleanedErrorMessage(e));
155
+ else if (e instanceof Error) __hot_updater_cli_tools.p.log.error(e.message);
156
+ throw new Error("Failed to build the app. See the error above for details from Gradle.");
157
+ }
158
+ return findBuildDirectory({
159
+ androidProjectPath,
160
+ moduleName: appModuleName,
161
+ tasks
162
+ });
163
+ }
164
+ /**
165
+ * Find the build output directory and artifact path
166
+ */
167
+ async function findBuildDirectory({ moduleName, tasks, androidProjectPath }) {
168
+ const selectedTask = tasks.find((t) => t.startsWith("install") || t.startsWith("assemble") || t.startsWith("bundle"));
169
+ if (!selectedTask) throw new Error(`Not supported gradle task: ${tasks.join(", ")}`);
170
+ const variantFromSelectedTask = selectedTask?.replace("install", "")?.replace("assemble", "")?.replace("bundle", "").split(/(?=[A-Z])/);
171
+ const variantPath = variantFromSelectedTask?.join("/")?.toLowerCase();
172
+ const variant = variantFromSelectedTask?.join("-")?.toLowerCase();
173
+ const isAabOutput = selectedTask?.includes("bundle") === true;
174
+ const buildDirectory = `${androidProjectPath}/${moduleName}/build/outputs/${isAabOutput ? "bundle" : "apk"}/${variantPath}`;
175
+ if (!buildDirectory) throw new Error("Failed to find Android gradle build directory.");
176
+ return {
177
+ buildDirectory,
178
+ buildArtifactPath: await getBuildOutputFilePath({
179
+ aab: isAabOutput,
180
+ appModuleName: moduleName,
181
+ buildDirectory,
182
+ variant
183
+ })
184
+ };
185
+ }
186
+ /**
187
+ * Get the path to the build output file (APK or AAB)
188
+ */
189
+ async function getBuildOutputFilePath({ aab, appModuleName, buildDirectory, variant }) {
190
+ const outputFile = `${appModuleName}-${variant}.${aab ? "aab" : "apk"}`;
191
+ const outputFilePath = path.default.join(buildDirectory, outputFile);
192
+ if (fs.default.existsSync(outputFilePath)) return outputFilePath;
193
+ __hot_updater_cli_tools.p.log.error(`Failed to find the output file for ${outputFilePath}`);
194
+ process.exit(1);
195
+ }
196
+
197
+ //#endregion
198
+ //#region src/utils/enrichNativeBuildAndroidScheme.ts
199
+ const enrichNativeBuildAndroidScheme = async ({ schemeConfig }) => {
200
+ return {
201
+ aab: true,
202
+ variant: "Release",
203
+ appModuleName: "app",
204
+ applicationId: schemeConfig.packageName,
205
+ ...schemeConfig
206
+ };
207
+ };
208
+
209
+ //#endregion
210
+ //#region src/buildAndroid.ts
211
+ const buildAndroid = async ({ schemeConfig: _schemeConfig }) => {
212
+ const androidProjectPath = path.default.join((0, __hot_updater_cli_tools.getCwd)(), "android");
213
+ const minBundleId = (0, __hot_updater_plugin_core.generateMinBundleId)();
214
+ const schemeConfig = await enrichNativeBuildAndroidScheme({ schemeConfig: _schemeConfig });
215
+ return runGradle({
216
+ args: { extraParams: [`-PMIN_BUNDLE_ID=${minBundleId}`] },
217
+ appModuleName: schemeConfig.appModuleName,
218
+ tasks: schemeConfig.aab ? [`bundle${schemeConfig.variant}`] : [`assemble${schemeConfig.variant}`],
219
+ androidProjectPath
220
+ });
221
+ };
222
+
223
+ //#endregion
224
+ //#region src/utils/device.ts
225
+ /**
226
+ * Get the path to adb executable from ANDROID_HOME or use system adb
227
+ */
228
+ const getAdbPath = () => {
229
+ return process.env["ANDROID_HOME"] ? path.default.join(process.env["ANDROID_HOME"], "platform-tools", "adb") : "adb";
230
+ };
231
+ /**
232
+ * Parses the output of the 'adb devices' command
233
+ */
234
+ const parseDevicesResult = ({ result }) => {
235
+ if (!result) return [];
236
+ const devices = [];
237
+ const lines = result.trim().split(/\r?\n/);
238
+ for (const line of lines) {
239
+ const [device, state] = line.split(/[ ,\t]+/).filter((w) => w !== "");
240
+ if (device && state === "device") devices.push(device);
241
+ }
242
+ return devices;
243
+ };
244
+ /**
245
+ * Executes the commands needed to get a list of devices from ADB
246
+ */
247
+ const getConnectedDevices = async () => {
248
+ const adbPath = getAdbPath();
249
+ try {
250
+ const { stdout } = await (0, execa.execa)(adbPath, ["devices"]);
251
+ return parseDevicesResult({ result: stdout });
252
+ } catch {
253
+ return [];
254
+ }
255
+ };
256
+ /**
257
+ * Runs ADB reverse tcp:8081 tcp:8081 to allow loading the jsbundle from the packager
258
+ * Set up port forwarding from device to development server using adb reverse
259
+ */
260
+ async function tryRunAdbReverse({ port = 8081, deviceId }) {
261
+ try {
262
+ const adbPath = getAdbPath();
263
+ const adbArgs = [
264
+ "-s",
265
+ deviceId,
266
+ "reverse",
267
+ `tcp:${port}`,
268
+ `tcp:${port}`
269
+ ];
270
+ __hot_updater_cli_tools.p.log.info(`Connecting "${deviceId}" to the development server`);
271
+ await (0, execa.execa)(adbPath, adbArgs);
272
+ } catch (_error) {
273
+ throw new Error(`Failed to connect "${deviceId}" to development server using "adb reverse"`);
274
+ }
275
+ }
276
+ /**
277
+ * Get name of Android emulator
278
+ */
279
+ const getEmulatorName = async (deviceId) => {
280
+ const { stdout } = await (0, execa.execa)(getAdbPath(), [
281
+ "-s",
282
+ deviceId,
283
+ "emu",
284
+ "avd",
285
+ "name"
286
+ ], { stdio: "pipe" });
287
+ if (!stdout) return "";
288
+ return stdout.split(node_os.default.EOL)[0].replace(/(\r\n|\n|\r)/gm, "").trim();
289
+ };
290
+ /**
291
+ * Get Android device name in readable format
292
+ */
293
+ const getPhoneName = async (deviceId) => {
294
+ const { stdout } = await (0, execa.execa)(getAdbPath(), [
295
+ "-s",
296
+ deviceId,
297
+ "shell",
298
+ "getprop",
299
+ "ro.product.model"
300
+ ], { stdio: "pipe" });
301
+ return stdout.replace(/\[ro\.product\.model\]:\s*\[(.*)\]/, "$1").trim();
302
+ };
303
+ /**
304
+ * Check if emulator has finished booting
305
+ */
306
+ const isEmulatorBooted = async (device) => {
307
+ const adbPath = getAdbPath();
308
+ const adbArgs = [
309
+ "-s",
310
+ device,
311
+ "shell",
312
+ "getprop",
313
+ "sys.boot_completed"
314
+ ];
315
+ try {
316
+ const { stdout } = await (0, execa.execa)(adbPath, adbArgs, { stdio: "pipe" });
317
+ return stdout.trim() === "1";
318
+ } catch {
319
+ return false;
320
+ }
321
+ };
322
+ const emulatorCommand = process.env["ANDROID_HOME"] ? `${process.env["ANDROID_HOME"]}/emulator/emulator` : "emulator";
323
+ /**
324
+ * Get a list of available Android emulators
325
+ */
326
+ const getEmulatorNames = async () => {
327
+ try {
328
+ const { stdout } = await (0, execa.execa)(emulatorCommand, ["-list-avds"], { stdio: "pipe" });
329
+ return stdout.split(node_os.default.EOL).filter((name) => name !== "" && !name.includes(" "));
330
+ } catch {
331
+ return [];
332
+ }
333
+ };
334
+ const checkEmulatorBootStatus = async ({ port, emulatorName, loader, onBooted }) => {
335
+ const connected = (await getConnectedDevices()).find((d) => d.includes(`${port}`));
336
+ if (connected) {
337
+ loader.message(`Emulator "${emulatorName}" is connected. Waiting for boot`);
338
+ if (await isEmulatorBooted(connected)) onBooted(connected);
339
+ }
340
+ };
341
+ /**
342
+ * Launch the Android emulator and wait for it to boot
343
+ */
344
+ const launchEmulator = async (emulatorName, port, loader) => {
345
+ const manualCommand = `${emulatorCommand} -avd ${emulatorName}`;
346
+ const timeout = 120;
347
+ const cp = (0, execa.execa)(emulatorCommand, [
348
+ "-avd",
349
+ emulatorName,
350
+ "-port",
351
+ `${port}`
352
+ ], {
353
+ detached: true,
354
+ stdio: "ignore"
355
+ });
356
+ cp.unref();
357
+ return new Promise((resolve, reject) => {
358
+ let bootCheckInterval;
359
+ let rejectTimeout;
360
+ const cleanup = () => {
361
+ clearTimeout(rejectTimeout);
362
+ clearInterval(bootCheckInterval);
363
+ };
364
+ const handleError = (message) => {
365
+ cleanup();
366
+ reject(new Error(message));
367
+ };
368
+ bootCheckInterval = setInterval(async () => {
369
+ await checkEmulatorBootStatus({
370
+ port,
371
+ emulatorName,
372
+ loader,
373
+ onBooted: (deviceId) => {
374
+ cleanup();
375
+ resolve(deviceId);
376
+ }
377
+ });
378
+ }, 1e3);
379
+ rejectTimeout = setTimeout(() => {
380
+ handleError(`It took too long to start and connect with Android emulator: ${emulatorName}. You can try starting the emulator manually from the terminal with: ${manualCommand}`);
381
+ }, timeout * 1e3);
382
+ cp.catch((error) => {
383
+ handleError(error);
384
+ });
385
+ });
386
+ };
387
+ const defaultPort = 5554;
388
+ const maxPort = 5682;
389
+ /**
390
+ * Find an available port for emulator (starting from 5554)
391
+ */
392
+ const getAvailableDevicePort = async (startPort = defaultPort) => {
393
+ /**
394
+ * The default value is 5554 for the first virtual device instance running on your machine.
395
+ * A virtual device normally occupies a pair of adjacent ports: a console port and an adb port.
396
+ * The console of the first virtual device running on a particular machine uses console port 5554 and adb port 5555.
397
+ * Subsequent instances use port numbers increasing by two.
398
+ * For example, 5556/5557, 5558/5559, and so on. The range is 5554 to 5682, allowing for 64 concurrent virtual devices.
399
+ */
400
+ const devices = await getConnectedDevices();
401
+ for (let port = startPort; port <= maxPort; port += 2) if (!devices.some((d) => d.includes(port.toString()))) return port;
402
+ throw new Error("Failed to launch emulator: no available ports");
403
+ };
404
+ /**
405
+ * Try to launch an emulator (or find the first available one)
406
+ */
407
+ const tryLaunchEmulator = async (name) => {
408
+ const port = await getAvailableDevicePort();
409
+ const loader = __hot_updater_cli_tools.p.spinner();
410
+ loader.start("Looking for available emulators");
411
+ const emulators = await getEmulatorNames();
412
+ const emulatorName = name ?? emulators[0];
413
+ let deviceId;
414
+ if (emulators.length > 0) try {
415
+ loader.message(`Launching emulator "${emulatorName}"`);
416
+ deviceId = await launchEmulator(emulatorName, port, loader);
417
+ loader.stop(`Launched ${emulatorName} emulator.`);
418
+ } catch (error) {
419
+ loader.stop(`Failed to launch ${emulatorName} emulator. ${error.message}`, 1);
420
+ }
421
+ else loader.stop("No emulators found as an output of `emulator -list-avds`. Please launch an emulator manually or connect a device", 1);
422
+ return deviceId;
423
+ };
424
+ /**
425
+ * List all Android devices and emulators (connected and available)
426
+ */
427
+ const listDevices = async () => {
428
+ const devices = await getConnectedDevices();
429
+ const allDevices = [];
430
+ for (const deviceId of devices) if (deviceId.includes("emulator")) {
431
+ const emulatorData = {
432
+ deviceId,
433
+ readableName: await getEmulatorName(deviceId),
434
+ connected: true,
435
+ type: "emulator"
436
+ };
437
+ allDevices.push(emulatorData);
438
+ } else {
439
+ const phoneData = {
440
+ deviceId,
441
+ readableName: await getPhoneName(deviceId),
442
+ type: "phone",
443
+ connected: true
444
+ };
445
+ allDevices.push(phoneData);
446
+ }
447
+ const emulatorNames = await getEmulatorNames();
448
+ for (const emulatorName of emulatorNames) {
449
+ if (allDevices.some((device) => device.readableName === emulatorName)) continue;
450
+ const emulatorData = {
451
+ deviceId: void 0,
452
+ readableName: emulatorName,
453
+ type: "emulator",
454
+ connected: false
455
+ };
456
+ allDevices.push(emulatorData);
457
+ }
458
+ return allDevices.sort((a, b) => {
459
+ if (a.type === "phone" && b.type === "emulator") return -1;
460
+ if (a.type === "emulator" && b.type === "phone") return 1;
461
+ return 0;
462
+ });
463
+ };
464
+ const matchingDevice = (devices, deviceArg) => {
465
+ const deviceByName = devices.find((device) => device.readableName === deviceArg);
466
+ const deviceById = devices.find((d) => d.deviceId === deviceArg);
467
+ return deviceByName || deviceById;
468
+ };
469
+ const selectTargetDevice = async ({ interactive, deviceOption }) => {
470
+ const availableDevices = await listDevices();
471
+ if (deviceOption === true && !interactive) {
472
+ __hot_updater_cli_tools.p.log.error("you can't select device manually with non-interactive cli mode(without -i option).");
473
+ process.exit(1);
474
+ }
475
+ if (typeof deviceOption === "string") {
476
+ const matchedDevice = matchingDevice(availableDevices, deviceOption);
477
+ if (!matchedDevice) {
478
+ __hot_updater_cli_tools.p.log.error(`device '${deviceOption}' isn't included in the attached devices.`);
479
+ process.exit(1);
480
+ }
481
+ return matchedDevice;
482
+ }
483
+ if (interactive) {
484
+ if (!availableDevices.length) {
485
+ __hot_updater_cli_tools.p.log.error("you passed -d option but there is no attached adb devices.");
486
+ process.exit(1);
487
+ }
488
+ const device = await __hot_updater_cli_tools.p.select({
489
+ message: "Target Device",
490
+ options: availableDevices.map((d) => ({
491
+ value: d,
492
+ label: d.readableName
493
+ }))
494
+ });
495
+ if (__hot_updater_cli_tools.p.isCancel(device)) process.exit(1);
496
+ return device;
497
+ }
498
+ };
499
+ const Device = {
500
+ getAdbPath,
501
+ getConnectedDevices,
502
+ tryRunAdbReverse,
503
+ tryLaunchEmulator,
504
+ listDevices,
505
+ selectTargetDevice
506
+ };
507
+
508
+ //#endregion
509
+ //#region src/runner/tryInstallAppOnDevice.ts
510
+ const tryInstallAppOnDevice = async ({ device, apkPath }) => {
511
+ if (!device.deviceId) {
512
+ __hot_updater_cli_tools.p.log.warn(`No device with id "${device.deviceId}", skipping launching the app.`);
513
+ return;
514
+ }
515
+ const deviceId = device.deviceId;
516
+ const adbArgs = [
517
+ "-s",
518
+ deviceId,
519
+ "install",
520
+ "-r",
521
+ "-d"
522
+ ];
523
+ adbArgs.push(apkPath);
524
+ const adbPath = Device.getAdbPath();
525
+ const loader = __hot_updater_cli_tools.p.spinner();
526
+ loader.start(`Installing the app on ${device.readableName} (id: ${deviceId})`);
527
+ try {
528
+ await (0, execa.execa)(adbPath, adbArgs);
529
+ loader.stop(`Installed the app on ${device.readableName} (id: ${deviceId}).`);
530
+ } catch (error) {
531
+ const errorMessage = error.stdout || error.stderr || "Unknown error";
532
+ if (typeof errorMessage === "string" && errorMessage.includes("INSTALL_FAILED_INSUFFICIENT_STORAGE")) loader.message("Installation failed due to insufficient storage");
533
+ loader.stop(`Failed: Installing the app on ${device.readableName} (id: ${deviceId})`, 1);
534
+ throw new Error(typeof errorMessage === "string" ? errorMessage : "Installation failed");
535
+ }
536
+ };
537
+
538
+ //#endregion
539
+ //#region src/runner/tryLaunchAppOnDevice.ts
540
+ async function tryLaunchAppOnDevice({ device, port, mainActivity, applicationId, packageName }) {
541
+ if (!device.deviceId) {
542
+ __hot_updater_cli_tools.p.log.warn(`No device with id "${device.deviceId}", skipping launching the app.`);
543
+ return;
544
+ }
545
+ const deviceId = device.deviceId;
546
+ await Device.tryRunAdbReverse({
547
+ deviceId,
548
+ port
549
+ });
550
+ const activity = mainActivity || ".MainActivity";
551
+ const adbArgs = [
552
+ "shell",
553
+ "am",
554
+ "start",
555
+ "-n",
556
+ `${applicationId}/${activity.startsWith(packageName) || !activity.startsWith(".") && activity.includes(".") ? activity : activity.startsWith(".") ? [packageName, activity].join("") : [packageName, activity].filter(Boolean).join(".")}`,
557
+ "-a",
558
+ "android.intent.action.MAIN",
559
+ "-c",
560
+ "android.intent.category.LAUNCHER"
561
+ ];
562
+ adbArgs.unshift("-s", deviceId);
563
+ const adbPath = Device.getAdbPath();
564
+ console.debug(`Running ${adbPath} ${adbArgs.join(" ")}.`);
565
+ const loader = __hot_updater_cli_tools.p.spinner();
566
+ loader.start(`Launching the app on ${device.readableName} (id: ${deviceId})`);
567
+ try {
568
+ await (0, execa.execa)(adbPath, adbArgs);
569
+ loader.stop(`Launched the app on ${device.readableName} (id: ${deviceId}) and listening on port ${port}.`);
570
+ } catch (_error) {
571
+ loader.stop("Failed to launch the app.", 1);
572
+ throw new Error(`Failed to launch the app on ${device.readableName}`);
573
+ }
574
+ }
575
+
576
+ //#endregion
577
+ //#region src/runAndroid.ts
578
+ const runAndroid = async ({ schemeConfig: _schemeConfig, runOption }) => {
579
+ const { interactive, device: deviceOption } = runOption;
580
+ const androidProjectPath = path.default.join((0, __hot_updater_cli_tools.getCwd)(), "android");
581
+ const schemeConfig = await enrichNativeBuildAndroidScheme({ schemeConfig: _schemeConfig });
582
+ if (schemeConfig.aab) {
583
+ __hot_updater_cli_tools.p.log.error("aab scheme can't not be run");
584
+ process.exit(1);
585
+ }
586
+ const device = await Device.selectTargetDevice({
587
+ deviceOption,
588
+ interactive
589
+ });
590
+ if (device) {
591
+ if (!(await Device.getConnectedDevices()).includes(device.deviceId || "")) {
592
+ if (device.type === "emulator") device.deviceId = await Device.tryLaunchEmulator(device.readableName);
593
+ }
594
+ if (!device.deviceId) throw new Error("Failed to run on any device");
595
+ } else if ((await Device.getConnectedDevices()).length === 0) {
596
+ __hot_updater_cli_tools.p.log.info("No devices found. Launching first available emulator.");
597
+ await Device.tryLaunchEmulator();
598
+ }
599
+ const task = `assemble${schemeConfig.variant}`;
600
+ const result = await runGradle({
601
+ args: { extraParams: [`-PMIN_BUNDLE_ID=${(0, __hot_updater_plugin_core.generateMinBundleId)()}`] },
602
+ appModuleName: schemeConfig.appModuleName,
603
+ tasks: [task],
604
+ androidProjectPath
605
+ });
606
+ const targetDevices = device ? [device] : (await Device.listDevices()).filter((d) => d.connected);
607
+ for (const targetDevice of targetDevices) {
608
+ await tryInstallAppOnDevice({
609
+ apkPath: result.buildArtifactPath,
610
+ device: targetDevice
611
+ });
612
+ await tryLaunchAppOnDevice({
613
+ applicationId: schemeConfig.applicationId,
614
+ device: targetDevice,
615
+ mainActivity: runOption.mainActivity,
616
+ packageName: schemeConfig.packageName,
617
+ port: runOption.port
618
+ });
619
+ }
620
+ __hot_updater_cli_tools.p.outro("Success 🎉");
621
+ return result;
622
+ };
623
+
624
+ //#endregion
625
+ exports.buildAndroid = buildAndroid;
626
+ exports.runAndroid = runAndroid;
@@ -0,0 +1,33 @@
1
+ import { NativeBuildAndroidScheme, NativeBuildOptions } from "@hot-updater/plugin-core";
2
+
3
+ //#region src/buildAndroid.d.ts
4
+ declare const buildAndroid: ({
5
+ schemeConfig: _schemeConfig
6
+ }: {
7
+ schemeConfig: NativeBuildAndroidScheme;
8
+ }) => Promise<{
9
+ buildDirectory: string;
10
+ buildArtifactPath: string;
11
+ }>;
12
+ //#endregion
13
+ //#region src/types.d.ts
14
+ interface AndroidNativeRunOptions extends NativeBuildOptions {
15
+ device?: string | boolean;
16
+ port?: string;
17
+ mainActivity?: string;
18
+ user?: string;
19
+ }
20
+ //#endregion
21
+ //#region src/runAndroid.d.ts
22
+ declare const runAndroid: ({
23
+ schemeConfig: _schemeConfig,
24
+ runOption
25
+ }: {
26
+ schemeConfig: NativeBuildAndroidScheme;
27
+ runOption: AndroidNativeRunOptions;
28
+ }) => Promise<{
29
+ buildDirectory: string;
30
+ buildArtifactPath: string;
31
+ }>;
32
+ //#endregion
33
+ export { type AndroidNativeRunOptions, buildAndroid, runAndroid };
@@ -0,0 +1,33 @@
1
+ import { NativeBuildAndroidScheme, NativeBuildOptions } from "@hot-updater/plugin-core";
2
+
3
+ //#region src/buildAndroid.d.ts
4
+ declare const buildAndroid: ({
5
+ schemeConfig: _schemeConfig
6
+ }: {
7
+ schemeConfig: NativeBuildAndroidScheme;
8
+ }) => Promise<{
9
+ buildDirectory: string;
10
+ buildArtifactPath: string;
11
+ }>;
12
+ //#endregion
13
+ //#region src/types.d.ts
14
+ interface AndroidNativeRunOptions extends NativeBuildOptions {
15
+ device?: string | boolean;
16
+ port?: string;
17
+ mainActivity?: string;
18
+ user?: string;
19
+ }
20
+ //#endregion
21
+ //#region src/runAndroid.d.ts
22
+ declare const runAndroid: ({
23
+ schemeConfig: _schemeConfig,
24
+ runOption
25
+ }: {
26
+ schemeConfig: NativeBuildAndroidScheme;
27
+ runOption: AndroidNativeRunOptions;
28
+ }) => Promise<{
29
+ buildDirectory: string;
30
+ buildArtifactPath: string;
31
+ }>;
32
+ //#endregion
33
+ export { type AndroidNativeRunOptions, buildAndroid, runAndroid };
package/dist/index.js ADDED
@@ -0,0 +1,596 @@
1
+ import { BuildLogger, getCwd, p } from "@hot-updater/cli-tools";
2
+ import { generateMinBundleId } from "@hot-updater/plugin-core";
3
+ import path from "path";
4
+ import { ExecaError, execa } from "execa";
5
+ import fs from "fs";
6
+ import os from "node:os";
7
+
8
+ //#region src/utils/createGradleLogger.ts
9
+ const createGradleLogger = () => new BuildLogger({
10
+ failurePatterns: ["BUILD FAILED"],
11
+ importantLogPatterns: [
12
+ "error:",
13
+ "Error:",
14
+ "ERROR:",
15
+ "> Task :",
16
+ "BUILD FAILED",
17
+ "BUILD SUCCESSFUL",
18
+ "FAILURE: Build failed with an exception.",
19
+ "* What went wrong:",
20
+ "Execution failed for task",
21
+ "Could not resolve",
22
+ "Failed to",
23
+ "Unable to load script",
24
+ "Metro encountered an error",
25
+ "React Native CLI",
26
+ "Android resource compilation failed",
27
+ "Duplicate class",
28
+ "Program type already present",
29
+ "actionable tasks:"
30
+ ],
31
+ progressMapping: [
32
+ [[
33
+ "buildKotlinToolingMetadata",
34
+ "generateAutolinkingNewArchitectureFiles",
35
+ "generateCodegenSchemaFromJavaScript"
36
+ ], 5],
37
+ [[
38
+ "createBundleReleaseJsAndAssets",
39
+ "bundleReleaseJsAndAssets",
40
+ "bundleDebugJsAndAssets"
41
+ ], 15],
42
+ [[
43
+ "generateReleaseResValues",
44
+ "generateReleaseResources",
45
+ "mergeReleaseResources"
46
+ ], 25],
47
+ [[
48
+ "processReleaseResources",
49
+ "processReleaseManifest",
50
+ "parseReleaseLocalResources"
51
+ ], 35],
52
+ [["compileReleaseKotlin", "compileDebugKotlin"], 50],
53
+ [["compileReleaseJavaWithJavac", "compileDebugJavaWithJavac"], 60],
54
+ [[
55
+ "dexBuilderRelease",
56
+ "mergeDexRelease",
57
+ "transformClassesWithDex"
58
+ ], 75],
59
+ [[
60
+ "compressReleaseAssets",
61
+ "optimizeReleaseResources",
62
+ "mergeReleaseAssets"
63
+ ], 85],
64
+ [[
65
+ "packageRelease",
66
+ "packageDebug",
67
+ "assembleRelease",
68
+ "assembleDebug"
69
+ ], 90],
70
+ [["bundleRelease", "bundleDebug"], 95],
71
+ [[/\d+ actionable tasks:/], 100]
72
+ ]
73
+ });
74
+
75
+ //#endregion
76
+ //#region src/builder/runGradle.ts
77
+ /**
78
+ * Clean up gradle error message by removing common noise
79
+ */
80
+ const getCleanedErrorMessage = (error) => {
81
+ const gradleLinesToRemove = [
82
+ "FAILURE: Build failed with an exception.",
83
+ "* Try:",
84
+ "> Run with --stacktrace option to get the stack trace.",
85
+ "> Run with --info or --debug option to get more log output.",
86
+ "> Run with --scan to get full insights.",
87
+ "> Get more help at [undefined](https://help.gradle.org).",
88
+ "> Get more help at https://help.gradle.org.",
89
+ "BUILD FAILED"
90
+ ];
91
+ return error.message.split("\n").filter((line) => !gradleLinesToRemove.some((l) => line.includes(l))).join("\n").trim();
92
+ };
93
+ /**
94
+ * Generate gradle task names with module prefix
95
+ */
96
+ function getTaskNames(moduleName, tasks) {
97
+ return tasks.map((task) => `${moduleName}:${task}`);
98
+ }
99
+ /**
100
+ * Get gradle wrapper command based on platform
101
+ */
102
+ const getGradleWrapper = () => process.platform.startsWith("win") ? "gradlew.bat" : "./gradlew";
103
+ /**
104
+ * Run gradle build with specified tasks and arguments
105
+ */
106
+ async function runGradle({ tasks, args, androidProjectPath, appModuleName }) {
107
+ const gradleArgs = getTaskNames(appModuleName, tasks);
108
+ gradleArgs.push("-x", "lint");
109
+ if (args.extraParams) gradleArgs.push(...args.extraParams);
110
+ p.log.info(`Run Gradle Settings:
111
+ Project ${androidProjectPath}
112
+ App Moudle ${appModuleName}
113
+ Tasks ${tasks.join(", ")}
114
+ Args ${gradleArgs.join(" ")}
115
+ `);
116
+ if ("port" in args && args.port != null) gradleArgs.push(`-PreactNativeDevServerPort=${args.port}`);
117
+ const logger = createGradleLogger();
118
+ logger.start(`${appModuleName}`);
119
+ try {
120
+ const process$1 = execa(getGradleWrapper(), gradleArgs, { cwd: androidProjectPath });
121
+ for await (const line of process$1) if (line) logger.processLine(line);
122
+ logger.stop();
123
+ } catch (e) {
124
+ logger.stop("Build failed", false);
125
+ if (e instanceof ExecaError) p.log.error(getCleanedErrorMessage(e));
126
+ else if (e instanceof Error) p.log.error(e.message);
127
+ throw new Error("Failed to build the app. See the error above for details from Gradle.");
128
+ }
129
+ return findBuildDirectory({
130
+ androidProjectPath,
131
+ moduleName: appModuleName,
132
+ tasks
133
+ });
134
+ }
135
+ /**
136
+ * Find the build output directory and artifact path
137
+ */
138
+ async function findBuildDirectory({ moduleName, tasks, androidProjectPath }) {
139
+ const selectedTask = tasks.find((t) => t.startsWith("install") || t.startsWith("assemble") || t.startsWith("bundle"));
140
+ if (!selectedTask) throw new Error(`Not supported gradle task: ${tasks.join(", ")}`);
141
+ const variantFromSelectedTask = selectedTask?.replace("install", "")?.replace("assemble", "")?.replace("bundle", "").split(/(?=[A-Z])/);
142
+ const variantPath = variantFromSelectedTask?.join("/")?.toLowerCase();
143
+ const variant = variantFromSelectedTask?.join("-")?.toLowerCase();
144
+ const isAabOutput = selectedTask?.includes("bundle") === true;
145
+ const buildDirectory = `${androidProjectPath}/${moduleName}/build/outputs/${isAabOutput ? "bundle" : "apk"}/${variantPath}`;
146
+ if (!buildDirectory) throw new Error("Failed to find Android gradle build directory.");
147
+ return {
148
+ buildDirectory,
149
+ buildArtifactPath: await getBuildOutputFilePath({
150
+ aab: isAabOutput,
151
+ appModuleName: moduleName,
152
+ buildDirectory,
153
+ variant
154
+ })
155
+ };
156
+ }
157
+ /**
158
+ * Get the path to the build output file (APK or AAB)
159
+ */
160
+ async function getBuildOutputFilePath({ aab, appModuleName, buildDirectory, variant }) {
161
+ const outputFile = `${appModuleName}-${variant}.${aab ? "aab" : "apk"}`;
162
+ const outputFilePath = path.join(buildDirectory, outputFile);
163
+ if (fs.existsSync(outputFilePath)) return outputFilePath;
164
+ p.log.error(`Failed to find the output file for ${outputFilePath}`);
165
+ process.exit(1);
166
+ }
167
+
168
+ //#endregion
169
+ //#region src/utils/enrichNativeBuildAndroidScheme.ts
170
+ const enrichNativeBuildAndroidScheme = async ({ schemeConfig }) => {
171
+ return {
172
+ aab: true,
173
+ variant: "Release",
174
+ appModuleName: "app",
175
+ applicationId: schemeConfig.packageName,
176
+ ...schemeConfig
177
+ };
178
+ };
179
+
180
+ //#endregion
181
+ //#region src/buildAndroid.ts
182
+ const buildAndroid = async ({ schemeConfig: _schemeConfig }) => {
183
+ const androidProjectPath = path.join(getCwd(), "android");
184
+ const minBundleId = generateMinBundleId();
185
+ const schemeConfig = await enrichNativeBuildAndroidScheme({ schemeConfig: _schemeConfig });
186
+ return runGradle({
187
+ args: { extraParams: [`-PMIN_BUNDLE_ID=${minBundleId}`] },
188
+ appModuleName: schemeConfig.appModuleName,
189
+ tasks: schemeConfig.aab ? [`bundle${schemeConfig.variant}`] : [`assemble${schemeConfig.variant}`],
190
+ androidProjectPath
191
+ });
192
+ };
193
+
194
+ //#endregion
195
+ //#region src/utils/device.ts
196
+ /**
197
+ * Get the path to adb executable from ANDROID_HOME or use system adb
198
+ */
199
+ const getAdbPath = () => {
200
+ return process.env["ANDROID_HOME"] ? path.join(process.env["ANDROID_HOME"], "platform-tools", "adb") : "adb";
201
+ };
202
+ /**
203
+ * Parses the output of the 'adb devices' command
204
+ */
205
+ const parseDevicesResult = ({ result }) => {
206
+ if (!result) return [];
207
+ const devices = [];
208
+ const lines = result.trim().split(/\r?\n/);
209
+ for (const line of lines) {
210
+ const [device, state] = line.split(/[ ,\t]+/).filter((w) => w !== "");
211
+ if (device && state === "device") devices.push(device);
212
+ }
213
+ return devices;
214
+ };
215
+ /**
216
+ * Executes the commands needed to get a list of devices from ADB
217
+ */
218
+ const getConnectedDevices = async () => {
219
+ const adbPath = getAdbPath();
220
+ try {
221
+ const { stdout } = await execa(adbPath, ["devices"]);
222
+ return parseDevicesResult({ result: stdout });
223
+ } catch {
224
+ return [];
225
+ }
226
+ };
227
+ /**
228
+ * Runs ADB reverse tcp:8081 tcp:8081 to allow loading the jsbundle from the packager
229
+ * Set up port forwarding from device to development server using adb reverse
230
+ */
231
+ async function tryRunAdbReverse({ port = 8081, deviceId }) {
232
+ try {
233
+ const adbPath = getAdbPath();
234
+ const adbArgs = [
235
+ "-s",
236
+ deviceId,
237
+ "reverse",
238
+ `tcp:${port}`,
239
+ `tcp:${port}`
240
+ ];
241
+ p.log.info(`Connecting "${deviceId}" to the development server`);
242
+ await execa(adbPath, adbArgs);
243
+ } catch (_error) {
244
+ throw new Error(`Failed to connect "${deviceId}" to development server using "adb reverse"`);
245
+ }
246
+ }
247
+ /**
248
+ * Get name of Android emulator
249
+ */
250
+ const getEmulatorName = async (deviceId) => {
251
+ const { stdout } = await execa(getAdbPath(), [
252
+ "-s",
253
+ deviceId,
254
+ "emu",
255
+ "avd",
256
+ "name"
257
+ ], { stdio: "pipe" });
258
+ if (!stdout) return "";
259
+ return stdout.split(os.EOL)[0].replace(/(\r\n|\n|\r)/gm, "").trim();
260
+ };
261
+ /**
262
+ * Get Android device name in readable format
263
+ */
264
+ const getPhoneName = async (deviceId) => {
265
+ const { stdout } = await execa(getAdbPath(), [
266
+ "-s",
267
+ deviceId,
268
+ "shell",
269
+ "getprop",
270
+ "ro.product.model"
271
+ ], { stdio: "pipe" });
272
+ return stdout.replace(/\[ro\.product\.model\]:\s*\[(.*)\]/, "$1").trim();
273
+ };
274
+ /**
275
+ * Check if emulator has finished booting
276
+ */
277
+ const isEmulatorBooted = async (device) => {
278
+ const adbPath = getAdbPath();
279
+ const adbArgs = [
280
+ "-s",
281
+ device,
282
+ "shell",
283
+ "getprop",
284
+ "sys.boot_completed"
285
+ ];
286
+ try {
287
+ const { stdout } = await execa(adbPath, adbArgs, { stdio: "pipe" });
288
+ return stdout.trim() === "1";
289
+ } catch {
290
+ return false;
291
+ }
292
+ };
293
+ const emulatorCommand = process.env["ANDROID_HOME"] ? `${process.env["ANDROID_HOME"]}/emulator/emulator` : "emulator";
294
+ /**
295
+ * Get a list of available Android emulators
296
+ */
297
+ const getEmulatorNames = async () => {
298
+ try {
299
+ const { stdout } = await execa(emulatorCommand, ["-list-avds"], { stdio: "pipe" });
300
+ return stdout.split(os.EOL).filter((name) => name !== "" && !name.includes(" "));
301
+ } catch {
302
+ return [];
303
+ }
304
+ };
305
+ const checkEmulatorBootStatus = async ({ port, emulatorName, loader, onBooted }) => {
306
+ const connected = (await getConnectedDevices()).find((d) => d.includes(`${port}`));
307
+ if (connected) {
308
+ loader.message(`Emulator "${emulatorName}" is connected. Waiting for boot`);
309
+ if (await isEmulatorBooted(connected)) onBooted(connected);
310
+ }
311
+ };
312
+ /**
313
+ * Launch the Android emulator and wait for it to boot
314
+ */
315
+ const launchEmulator = async (emulatorName, port, loader) => {
316
+ const manualCommand = `${emulatorCommand} -avd ${emulatorName}`;
317
+ const timeout = 120;
318
+ const cp = execa(emulatorCommand, [
319
+ "-avd",
320
+ emulatorName,
321
+ "-port",
322
+ `${port}`
323
+ ], {
324
+ detached: true,
325
+ stdio: "ignore"
326
+ });
327
+ cp.unref();
328
+ return new Promise((resolve, reject) => {
329
+ let bootCheckInterval;
330
+ let rejectTimeout;
331
+ const cleanup = () => {
332
+ clearTimeout(rejectTimeout);
333
+ clearInterval(bootCheckInterval);
334
+ };
335
+ const handleError = (message) => {
336
+ cleanup();
337
+ reject(new Error(message));
338
+ };
339
+ bootCheckInterval = setInterval(async () => {
340
+ await checkEmulatorBootStatus({
341
+ port,
342
+ emulatorName,
343
+ loader,
344
+ onBooted: (deviceId) => {
345
+ cleanup();
346
+ resolve(deviceId);
347
+ }
348
+ });
349
+ }, 1e3);
350
+ rejectTimeout = setTimeout(() => {
351
+ handleError(`It took too long to start and connect with Android emulator: ${emulatorName}. You can try starting the emulator manually from the terminal with: ${manualCommand}`);
352
+ }, timeout * 1e3);
353
+ cp.catch((error) => {
354
+ handleError(error);
355
+ });
356
+ });
357
+ };
358
+ const defaultPort = 5554;
359
+ const maxPort = 5682;
360
+ /**
361
+ * Find an available port for emulator (starting from 5554)
362
+ */
363
+ const getAvailableDevicePort = async (startPort = defaultPort) => {
364
+ /**
365
+ * The default value is 5554 for the first virtual device instance running on your machine.
366
+ * A virtual device normally occupies a pair of adjacent ports: a console port and an adb port.
367
+ * The console of the first virtual device running on a particular machine uses console port 5554 and adb port 5555.
368
+ * Subsequent instances use port numbers increasing by two.
369
+ * For example, 5556/5557, 5558/5559, and so on. The range is 5554 to 5682, allowing for 64 concurrent virtual devices.
370
+ */
371
+ const devices = await getConnectedDevices();
372
+ for (let port = startPort; port <= maxPort; port += 2) if (!devices.some((d) => d.includes(port.toString()))) return port;
373
+ throw new Error("Failed to launch emulator: no available ports");
374
+ };
375
+ /**
376
+ * Try to launch an emulator (or find the first available one)
377
+ */
378
+ const tryLaunchEmulator = async (name) => {
379
+ const port = await getAvailableDevicePort();
380
+ const loader = p.spinner();
381
+ loader.start("Looking for available emulators");
382
+ const emulators = await getEmulatorNames();
383
+ const emulatorName = name ?? emulators[0];
384
+ let deviceId;
385
+ if (emulators.length > 0) try {
386
+ loader.message(`Launching emulator "${emulatorName}"`);
387
+ deviceId = await launchEmulator(emulatorName, port, loader);
388
+ loader.stop(`Launched ${emulatorName} emulator.`);
389
+ } catch (error) {
390
+ loader.stop(`Failed to launch ${emulatorName} emulator. ${error.message}`, 1);
391
+ }
392
+ else loader.stop("No emulators found as an output of `emulator -list-avds`. Please launch an emulator manually or connect a device", 1);
393
+ return deviceId;
394
+ };
395
+ /**
396
+ * List all Android devices and emulators (connected and available)
397
+ */
398
+ const listDevices = async () => {
399
+ const devices = await getConnectedDevices();
400
+ const allDevices = [];
401
+ for (const deviceId of devices) if (deviceId.includes("emulator")) {
402
+ const emulatorData = {
403
+ deviceId,
404
+ readableName: await getEmulatorName(deviceId),
405
+ connected: true,
406
+ type: "emulator"
407
+ };
408
+ allDevices.push(emulatorData);
409
+ } else {
410
+ const phoneData = {
411
+ deviceId,
412
+ readableName: await getPhoneName(deviceId),
413
+ type: "phone",
414
+ connected: true
415
+ };
416
+ allDevices.push(phoneData);
417
+ }
418
+ const emulatorNames = await getEmulatorNames();
419
+ for (const emulatorName of emulatorNames) {
420
+ if (allDevices.some((device) => device.readableName === emulatorName)) continue;
421
+ const emulatorData = {
422
+ deviceId: void 0,
423
+ readableName: emulatorName,
424
+ type: "emulator",
425
+ connected: false
426
+ };
427
+ allDevices.push(emulatorData);
428
+ }
429
+ return allDevices.sort((a, b) => {
430
+ if (a.type === "phone" && b.type === "emulator") return -1;
431
+ if (a.type === "emulator" && b.type === "phone") return 1;
432
+ return 0;
433
+ });
434
+ };
435
+ const matchingDevice = (devices, deviceArg) => {
436
+ const deviceByName = devices.find((device) => device.readableName === deviceArg);
437
+ const deviceById = devices.find((d) => d.deviceId === deviceArg);
438
+ return deviceByName || deviceById;
439
+ };
440
+ const selectTargetDevice = async ({ interactive, deviceOption }) => {
441
+ const availableDevices = await listDevices();
442
+ if (deviceOption === true && !interactive) {
443
+ p.log.error("you can't select device manually with non-interactive cli mode(without -i option).");
444
+ process.exit(1);
445
+ }
446
+ if (typeof deviceOption === "string") {
447
+ const matchedDevice = matchingDevice(availableDevices, deviceOption);
448
+ if (!matchedDevice) {
449
+ p.log.error(`device '${deviceOption}' isn't included in the attached devices.`);
450
+ process.exit(1);
451
+ }
452
+ return matchedDevice;
453
+ }
454
+ if (interactive) {
455
+ if (!availableDevices.length) {
456
+ p.log.error("you passed -d option but there is no attached adb devices.");
457
+ process.exit(1);
458
+ }
459
+ const device = await p.select({
460
+ message: "Target Device",
461
+ options: availableDevices.map((d) => ({
462
+ value: d,
463
+ label: d.readableName
464
+ }))
465
+ });
466
+ if (p.isCancel(device)) process.exit(1);
467
+ return device;
468
+ }
469
+ };
470
+ const Device = {
471
+ getAdbPath,
472
+ getConnectedDevices,
473
+ tryRunAdbReverse,
474
+ tryLaunchEmulator,
475
+ listDevices,
476
+ selectTargetDevice
477
+ };
478
+
479
+ //#endregion
480
+ //#region src/runner/tryInstallAppOnDevice.ts
481
+ const tryInstallAppOnDevice = async ({ device, apkPath }) => {
482
+ if (!device.deviceId) {
483
+ p.log.warn(`No device with id "${device.deviceId}", skipping launching the app.`);
484
+ return;
485
+ }
486
+ const deviceId = device.deviceId;
487
+ const adbArgs = [
488
+ "-s",
489
+ deviceId,
490
+ "install",
491
+ "-r",
492
+ "-d"
493
+ ];
494
+ adbArgs.push(apkPath);
495
+ const adbPath = Device.getAdbPath();
496
+ const loader = p.spinner();
497
+ loader.start(`Installing the app on ${device.readableName} (id: ${deviceId})`);
498
+ try {
499
+ await execa(adbPath, adbArgs);
500
+ loader.stop(`Installed the app on ${device.readableName} (id: ${deviceId}).`);
501
+ } catch (error) {
502
+ const errorMessage = error.stdout || error.stderr || "Unknown error";
503
+ if (typeof errorMessage === "string" && errorMessage.includes("INSTALL_FAILED_INSUFFICIENT_STORAGE")) loader.message("Installation failed due to insufficient storage");
504
+ loader.stop(`Failed: Installing the app on ${device.readableName} (id: ${deviceId})`, 1);
505
+ throw new Error(typeof errorMessage === "string" ? errorMessage : "Installation failed");
506
+ }
507
+ };
508
+
509
+ //#endregion
510
+ //#region src/runner/tryLaunchAppOnDevice.ts
511
+ async function tryLaunchAppOnDevice({ device, port, mainActivity, applicationId, packageName }) {
512
+ if (!device.deviceId) {
513
+ p.log.warn(`No device with id "${device.deviceId}", skipping launching the app.`);
514
+ return;
515
+ }
516
+ const deviceId = device.deviceId;
517
+ await Device.tryRunAdbReverse({
518
+ deviceId,
519
+ port
520
+ });
521
+ const activity = mainActivity || ".MainActivity";
522
+ const adbArgs = [
523
+ "shell",
524
+ "am",
525
+ "start",
526
+ "-n",
527
+ `${applicationId}/${activity.startsWith(packageName) || !activity.startsWith(".") && activity.includes(".") ? activity : activity.startsWith(".") ? [packageName, activity].join("") : [packageName, activity].filter(Boolean).join(".")}`,
528
+ "-a",
529
+ "android.intent.action.MAIN",
530
+ "-c",
531
+ "android.intent.category.LAUNCHER"
532
+ ];
533
+ adbArgs.unshift("-s", deviceId);
534
+ const adbPath = Device.getAdbPath();
535
+ console.debug(`Running ${adbPath} ${adbArgs.join(" ")}.`);
536
+ const loader = p.spinner();
537
+ loader.start(`Launching the app on ${device.readableName} (id: ${deviceId})`);
538
+ try {
539
+ await execa(adbPath, adbArgs);
540
+ loader.stop(`Launched the app on ${device.readableName} (id: ${deviceId}) and listening on port ${port}.`);
541
+ } catch (_error) {
542
+ loader.stop("Failed to launch the app.", 1);
543
+ throw new Error(`Failed to launch the app on ${device.readableName}`);
544
+ }
545
+ }
546
+
547
+ //#endregion
548
+ //#region src/runAndroid.ts
549
+ const runAndroid = async ({ schemeConfig: _schemeConfig, runOption }) => {
550
+ const { interactive, device: deviceOption } = runOption;
551
+ const androidProjectPath = path.join(getCwd(), "android");
552
+ const schemeConfig = await enrichNativeBuildAndroidScheme({ schemeConfig: _schemeConfig });
553
+ if (schemeConfig.aab) {
554
+ p.log.error("aab scheme can't not be run");
555
+ process.exit(1);
556
+ }
557
+ const device = await Device.selectTargetDevice({
558
+ deviceOption,
559
+ interactive
560
+ });
561
+ if (device) {
562
+ if (!(await Device.getConnectedDevices()).includes(device.deviceId || "")) {
563
+ if (device.type === "emulator") device.deviceId = await Device.tryLaunchEmulator(device.readableName);
564
+ }
565
+ if (!device.deviceId) throw new Error("Failed to run on any device");
566
+ } else if ((await Device.getConnectedDevices()).length === 0) {
567
+ p.log.info("No devices found. Launching first available emulator.");
568
+ await Device.tryLaunchEmulator();
569
+ }
570
+ const task = `assemble${schemeConfig.variant}`;
571
+ const result = await runGradle({
572
+ args: { extraParams: [`-PMIN_BUNDLE_ID=${generateMinBundleId()}`] },
573
+ appModuleName: schemeConfig.appModuleName,
574
+ tasks: [task],
575
+ androidProjectPath
576
+ });
577
+ const targetDevices = device ? [device] : (await Device.listDevices()).filter((d) => d.connected);
578
+ for (const targetDevice of targetDevices) {
579
+ await tryInstallAppOnDevice({
580
+ apkPath: result.buildArtifactPath,
581
+ device: targetDevice
582
+ });
583
+ await tryLaunchAppOnDevice({
584
+ applicationId: schemeConfig.applicationId,
585
+ device: targetDevice,
586
+ mainActivity: runOption.mainActivity,
587
+ packageName: schemeConfig.packageName,
588
+ port: runOption.port
589
+ });
590
+ }
591
+ p.outro("Success 🎉");
592
+ return result;
593
+ };
594
+
595
+ //#endregion
596
+ export { buildAndroid, runAndroid };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@hot-updater/android-helper",
3
+ "version": "0.25.4",
4
+ "type": "module",
5
+ "main": "dist/index.cjs",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.cjs"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "package.json"
18
+ ],
19
+ "description": "Android development helper utilities for hot-updater",
20
+ "license": "MIT",
21
+ "repository": "https://github.com/gronxb/hot-updater",
22
+ "author": "gronxb <gron1gh1@gmail.com> (https://github.com/gronxb)",
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "dependencies": {
27
+ "execa": "9.5.2",
28
+ "@hot-updater/core": "0.25.4",
29
+ "@hot-updater/cli-tools": "0.25.4",
30
+ "@hot-updater/plugin-core": "0.25.4"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^20"
34
+ },
35
+ "scripts": {
36
+ "build": "tsdown",
37
+ "test:type": "tsc --noEmit"
38
+ }
39
+ }