@expo-harmony/cli 55.0.26-harmony.11 → 55.0.26-harmony.12

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/README.md CHANGED
@@ -80,7 +80,7 @@ npx expo-harmony prebuild --check
80
80
 
81
81
  `--clean` 删除原生工程后重新生成。如果工程缺少 CNG 清单或模板标记,或者目录是符号链接、位于项目之外,命令会拒绝删除。
82
82
 
83
- `--check` 比较现有文件与当前配置生成的文件,不修改项目文件。没有差异时退出码为 0;有差异时列出变更,退出码为 2。它不能与其他会修改工程的选项一起使用。
83
+ `--check` 比较现有文件与当前配置生成的文件,不修改项目文件。没有差异时退出码为 0,有差异时列出变更并以退出码 2 结束。这个选项不能和会修改工程的选项一起使用。
84
84
 
85
85
  ## Build
86
86
 
@@ -94,7 +94,7 @@ npx expo-harmony build --sync
94
94
 
95
95
  默认构建 Debug 版本。使用 `--variant release` 构建 Release 版本时,命令会将 JS Bundle 和资源打包进应用。
96
96
 
97
- 项目没有原生工程时,命令会先运行 prebuild。已有 CNG 工程时,会检查生成的文件是否需要更新;有差异时停止构建,可以使用 `--sync` 重新生成后继续构建。bare 工程按现有原生配置构建,不支持 `--sync`。
97
+ 项目没有原生工程时,命令会先运行 prebuild。已有 CNG 工程时,会检查生成的文件是否需要更新,有差异就停止构建,可以用 `--sync` 重新生成后继续构建。bare 工程按现有原生配置构建,不支持 `--sync`。
98
98
 
99
99
  ## Doctor
100
100
 
@@ -154,7 +154,7 @@ npx expo-harmony run \
154
154
  --port 8081
155
155
  ```
156
156
 
157
- `run` 构建 HAP,在设备或模拟器上安装并启动应用。它使用项目中已有的原生工程;如果工程不存在,会先运行 prebuild。
157
+ `run` 构建 HAP,在设备或模拟器上安装并启动应用。它使用项目中已有的原生工程,工程不存在时先运行 prebuild。
158
158
 
159
159
  常用选项:
160
160
 
@@ -169,7 +169,7 @@ npx expo-harmony run \
169
169
 
170
170
  Debug 模式下,如果 Metro 由这个命令启动,日志会显示在当前终端,按 Ctrl+C 退出。
171
171
 
172
- 未指定 `--device` 时,优先使用已连接的设备。没有连接的设备时,先等待正在启动的模拟器;如果也没有正在启动的模拟器,就启动本地唯一的模拟器。有多个可选设备或模拟器时,需要用 `--device` 指定。
172
+ 未指定 `--device` 时,优先使用已连接的设备。没有连接的设备时,先等待正在启动的模拟器,没有的话就启动本地唯一的模拟器。有多个可选设备或模拟器时,需要用 `--device` 指定。
173
173
 
174
174
  按名称选择或自动启动模拟器时,CLI 会等待模拟器开机并连接 HDC,最多等待 120 秒。退出 CLI 或 Metro 后,模拟器继续运行。
175
175
 
@@ -191,6 +191,28 @@ CLI 先读取环境变量指定的工具路径,再查找 DevEco Studio 的安
191
191
  | `EXPO_HARMONY_NODE` | 指定原生构建过程中运行 Expo Harmony CLI 的 Node.js 路径,要求 Node.js 20 或更高版本 |
192
192
  | `DEVECO_SDK_HOME`、`HARMONY_HOME` 或 `OHOS_SDK_HOME` | 指定 HarmonyOS SDK 根目录 |
193
193
 
194
+ ### 超时
195
+
196
+ OHPM 安装、Hvigor 构建、等待模拟器这些步骤都有超时,默认值见下表,可以通过环境变量覆盖。变量名为 `EXPO_HARMONY_<操作名大写下划线>_TIMEOUT_MS`,单位为毫秒,例如 `EXPO_HARMONY_OHPM_INSTALL_TIMEOUT_MS`。设置成 0、负数或无法解析的值时,这个变量会被忽略,并给出警告。
197
+
198
+ | 环境变量 | 对应操作 | 默认值 |
199
+ | --- | --- | --- |
200
+ | `EXPO_HARMONY_OHPM_INSTALL_TIMEOUT_MS` | OHPM 依赖安装 | 30 分钟 |
201
+ | `EXPO_HARMONY_HVIGOR_BUILD_TIMEOUT_MS` | Hvigor 构建 | 15 分钟 |
202
+ | `EXPO_HARMONY_EXPO_EXPORT_EMBED_TIMEOUT_MS` | `export:embed`(Hermes 打包) | 10 分钟 |
203
+ | `EXPO_HARMONY_METRO_READY_TIMEOUT_MS` | 等待 Metro 就绪 | 30 分钟 |
204
+ | `EXPO_HARMONY_START_EMULATOR_TIMEOUT_MS` | 等待模拟器连接 HDC | 2 分钟 |
205
+ | `EXPO_HARMONY_INSTALL_HAP_TIMEOUT_MS` | 安装 HAP 到设备 | 2 分钟 |
206
+ | `EXPO_HARMONY_LAUNCH_APP_TIMEOUT_MS` | 启动应用 | 30 秒 |
207
+ | `EXPO_HARMONY_LIST_DEVICES_TIMEOUT_MS` | HDC 列出设备 | 15 秒 |
208
+ | `EXPO_HARMONY_LIST_EMULATORS_TIMEOUT_MS` | 列出模拟器实例 | 15 秒 |
209
+ | `EXPO_HARMONY_FORCE_STOP_APP_TIMEOUT_MS` | 强制停止应用 | 15 秒 |
210
+ | `EXPO_HARMONY_LIST_METRO_PORTS_TIMEOUT_MS` | 列出 Metro 端口转发 | 15 秒 |
211
+ | `EXPO_HARMONY_REMOVE_METRO_PORT_TIMEOUT_MS` | 移除 Metro 端口转发 | 15 秒 |
212
+ | `EXPO_HARMONY_REVERSE_METRO_PORT_TIMEOUT_MS` | 反向转发 Metro 端口 | 15 秒 |
213
+ | `EXPO_HARMONY_HDC_TIMEOUT_MS` | 其他 HDC 命令 | 60 秒 |
214
+ | `EXPO_HARMONY_DOCTOR_TIMEOUT_MS` | Doctor 工具检查 | 10 秒 |
215
+
194
216
  ## Author
195
217
 
196
218
  **expo-harmony** © [Baoshuo](https://github.com/renbaoshuo), Released under the MIT License.<br>
@@ -13,6 +13,7 @@ const config_plugins_1 = require("@expo-harmony/config-plugins");
13
13
  const expo_modules_autolinking_1 = require("@expo-harmony/expo-modules-autolinking");
14
14
  const internal_1 = require("@expo-harmony/prebuild-config/internal");
15
15
  const process_1 = require("../process");
16
+ const timeout_1 = require("../timeout");
16
17
  const projectLock_1 = require("../projectLock");
17
18
  const project_1 = require("../native/project");
18
19
  const toolchain_1 = require("../native/toolchain");
@@ -179,7 +180,7 @@ async function doctorUnlockedAsync(root, options = {}) {
179
180
  capture: true,
180
181
  cwd: root,
181
182
  operation: `doctor-${id}`,
182
- timeoutMs: 10_000,
183
+ timeoutMs: (0, timeout_1.resolveTimeoutMs)('doctor', 10_000),
183
184
  });
184
185
  checks.push(result.code === 0 && !result.timedOut
185
186
  ? check(id, 'pass', `${command} is available through ${tool.source}.`)
@@ -15,6 +15,7 @@ const errors_1 = require("../errors");
15
15
  const projectLock_1 = require("../projectLock");
16
16
  const project_1 = require("../native/project");
17
17
  const process_1 = require("../process");
18
+ const timeout_1 = require("../timeout");
18
19
  const expo_1 = require("../expo");
19
20
  const manifest_1 = require("./manifest");
20
21
  async function exportEmbedUnlockedAsync(root, options = {}) {
@@ -63,7 +64,7 @@ async function exportEmbedUnlockedAsync(root, options = {}) {
63
64
  },
64
65
  operation: 'expo-export-embed',
65
66
  outputLimit: 4 * 1024 * 1024,
66
- timeoutMs: options.timeoutMs || 10 * 60_000,
67
+ timeoutMs: (0, timeout_1.resolveTimeoutMs)('expo-export-embed', 10 * 60_000, options.timeoutMs),
67
68
  });
68
69
  if (result.code !== 0 || result.timedOut) {
69
70
  const diagnostics = (0, process_1.formatDiagnostics)(result);
@@ -15,6 +15,7 @@ const log_1 = require("../log");
15
15
  const path_1 = require("../path");
16
16
  const process_1 = require("../process");
17
17
  const profile_1 = require("../profile");
18
+ const timeout_1 = require("../timeout");
18
19
  async function buildNativeAsync(root, plan, tools, options, steps) {
19
20
  let exported = null;
20
21
  const embedded = plan.buildMode === 'release' || (0, config_1.getConfig)(root).exp.updates?.useNativeDebug === true;
@@ -59,7 +60,7 @@ async function buildNativeAsync(root, plan, tools, options, steps) {
59
60
  },
60
61
  message: 'Hvigor build',
61
62
  operation: 'hvigor-build',
62
- timeoutMs: 15 * 60_000,
63
+ timeoutMs: (0, timeout_1.resolveTimeoutMs)('hvigor-build', 15 * 60_000),
63
64
  }));
64
65
  if (!(0, file_1.isNonEmptyRegularFile)(plan.expectedHap)) {
65
66
  throw new errors_1.HarmonyCliError('ERR_HARMONY_HAP_MISSING', 'Hvigor completed without producing the expected non-empty regular HAP.', { operation: 'verify-hap' });
@@ -2,12 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.installHarmonyDependenciesAsync = installHarmonyDependenciesAsync;
4
4
  const process_1 = require("../process");
5
+ const timeout_1 = require("../timeout");
5
6
  async function installHarmonyDependenciesAsync(plan, tools, options = {}) {
6
7
  await (0, process_1.runCheckedAsync)(tools.ohpm.command, [...tools.ohpm.args, 'install', '--all'], {
7
8
  code: 'ERR_HARMONY_OHPM_FAILED',
8
9
  cwd: plan.harmonyRoot,
9
10
  message: 'OHPM install',
10
11
  operation: 'ohpm-install',
11
- timeoutMs: options.timeoutMs || 5 * 60_000,
12
+ timeoutMs: (0, timeout_1.resolveTimeoutMs)('ohpm-install', 30 * 60_000, options.timeoutMs),
12
13
  });
13
14
  }
@@ -8,6 +8,7 @@ const promises_1 = require("node:timers/promises");
8
8
  const emulators_1 = require("./emulators");
9
9
  const errors_1 = require("../errors");
10
10
  const process_1 = require("../process");
11
+ const timeout_1 = require("../timeout");
11
12
  function parseHdcTargets(output) {
12
13
  const trimmed = String(output).trim();
13
14
  if (!trimmed || trimmed === '[Empty]')
@@ -42,7 +43,7 @@ async function runHdcAsync(hdc, args, options = {}) {
42
43
  cwd: options.cwd,
43
44
  operation: options.operation || 'hdc',
44
45
  outputLimit: options.outputLimit || 256 * 1024,
45
- timeoutMs: options.timeoutMs || 60_000,
46
+ timeoutMs: (0, timeout_1.resolveTimeoutMs)(options.operation || 'hdc', 60_000, options.timeoutMs),
46
47
  });
47
48
  if (!options.allowFailure && hasCommandFailure(result)) {
48
49
  const diagnostics = (0, process_1.formatDiagnostics)(result, 2_000);
@@ -56,7 +57,7 @@ async function listConnectedDevicesAsync(hdc, options) {
56
57
  cwd: options.cwd,
57
58
  message: 'Cannot list Harmony devices',
58
59
  operation: 'list-devices',
59
- timeoutMs: options.timeoutMs || 15_000,
60
+ timeoutMs: (0, timeout_1.resolveTimeoutMs)('list-devices', 15_000, options.timeoutMs),
60
61
  });
61
62
  const targets = parseHdcTargets(result.stdout || result.stderr);
62
63
  return targets.filter(target => target.state.toLowerCase() === 'connected');
@@ -110,7 +111,7 @@ async function selectDeviceAsync(hdc, requested, options = {}) {
110
111
  launch = (0, emulators_1.startEmulator)(options.emulator, instance.name, options.emulatorLogFile, options.cwd);
111
112
  }
112
113
  options.onProgress?.(`Waiting for Harmony emulator ${instance.name} to connect`);
113
- const deadline = Date.now() + (options.timeoutMs || 120_000);
114
+ const deadline = Date.now() + (0, timeout_1.resolveTimeoutMs)('start-emulator', 120_000, options.timeoutMs);
114
115
  while (Date.now() < deadline) {
115
116
  launch?.assertRunning();
116
117
  const devices = await listConnectedDevicesAsync(hdc, {
@@ -155,7 +156,7 @@ async function installHapAsync(hdc, device, hap, options = {}) {
155
156
  cwd: options.cwd,
156
157
  message: `Cannot install the Harmony HAP on ${device.id}`,
157
158
  operation: 'install-hap',
158
- timeoutMs: options.timeoutMs || 2 * 60_000,
159
+ timeoutMs: (0, timeout_1.resolveTimeoutMs)('install-hap', 2 * 60_000, options.timeoutMs),
159
160
  });
160
161
  }
161
162
  async function configureMetroPortAsync(hdc, device, port, options = {}) {
@@ -171,7 +172,7 @@ async function configureMetroPortAsync(hdc, device, port, options = {}) {
171
172
  const forwards = await runHdcAsync(hdc, ['-t', device.id, 'fport', 'ls'], {
172
173
  cwd: options.cwd,
173
174
  operation: 'list-metro-ports',
174
- timeoutMs: 15_000,
175
+ timeoutMs: (0, timeout_1.resolveTimeoutMs)('list-metro-ports', 15_000, options.timeoutMs),
175
176
  });
176
177
  for (const line of forwards.stdout.split(/\r?\n/u)) {
177
178
  const [target, remote, local, direction] = line.trim().split(/\s+/u);
@@ -182,7 +183,7 @@ async function configureMetroPortAsync(hdc, device, port, options = {}) {
182
183
  await runHdcAsync(hdc, ['-t', device.id, 'fport', 'rm', remote, local], {
183
184
  cwd: options.cwd,
184
185
  operation: 'remove-metro-port',
185
- timeoutMs: 15_000,
186
+ timeoutMs: (0, timeout_1.resolveTimeoutMs)('remove-metro-port', 15_000, options.timeoutMs),
186
187
  });
187
188
  }
188
189
  await runHdcAsync(hdc, ['-t', device.id, 'rport', deviceEndpoint, hostEndpoint], {
@@ -190,7 +191,7 @@ async function configureMetroPortAsync(hdc, device, port, options = {}) {
190
191
  cwd: options.cwd,
191
192
  message: `Cannot reverse Metro port ${port} for ${device.id}`,
192
193
  operation: 'reverse-metro-port',
193
- timeoutMs: 15_000,
194
+ timeoutMs: (0, timeout_1.resolveTimeoutMs)('reverse-metro-port', 15_000, options.timeoutMs),
194
195
  });
195
196
  }
196
197
  async function launchAppAsync(hdc, device, bundleName, abilityName, options = {}) {
@@ -204,7 +205,7 @@ async function launchAppAsync(hdc, device, bundleName, abilityName, options = {}
204
205
  allowFailure: true,
205
206
  cwd: options.cwd,
206
207
  operation: 'force-stop-app',
207
- timeoutMs: 15_000,
208
+ timeoutMs: (0, timeout_1.resolveTimeoutMs)('force-stop-app', 15_000),
208
209
  });
209
210
  await runHdcAsync(hdc, [
210
211
  '-t', device.id,
@@ -218,6 +219,6 @@ async function launchAppAsync(hdc, device, bundleName, abilityName, options = {}
218
219
  cwd: options.cwd,
219
220
  message: `Cannot launch ${bundleName} on ${device.id}`,
220
221
  operation: 'launch-app',
221
- timeoutMs: 30_000,
222
+ timeoutMs: (0, timeout_1.resolveTimeoutMs)('launch-app', 30_000),
222
223
  });
223
224
  }
@@ -10,6 +10,7 @@ const node_path_1 = __importDefault(require("node:path"));
10
10
  const cross_spawn_1 = __importDefault(require("cross-spawn"));
11
11
  const errors_1 = require("../errors");
12
12
  const process_1 = require("../process");
13
+ const timeout_1 = require("../timeout");
13
14
  async function listEmulatorsAsync(tool, options = {}) {
14
15
  let result;
15
16
  try {
@@ -17,7 +18,7 @@ async function listEmulatorsAsync(tool, options = {}) {
17
18
  capture: true,
18
19
  cwd: options.cwd,
19
20
  operation: 'list-emulators',
20
- timeoutMs: options.timeoutMs || 15_000,
21
+ timeoutMs: (0, timeout_1.resolveTimeoutMs)('list-emulators', 15_000, options.timeoutMs),
21
22
  });
22
23
  }
23
24
  catch (cause) {
@@ -10,6 +10,7 @@ const session_1 = require("../development/session");
10
10
  const errors_1 = require("../errors");
11
11
  const process_1 = require("../process");
12
12
  const expo_1 = require("../expo");
13
+ const timeout_1 = require("../timeout");
13
14
  function delay(milliseconds) {
14
15
  return new Promise(resolve => setTimeout(resolve, milliseconds));
15
16
  }
@@ -107,7 +108,7 @@ async function startExpoMetroAsync(projectRoot, options) {
107
108
  exitError = error;
108
109
  });
109
110
  const startedAt = Date.now();
110
- const timeoutMs = options.readyTimeoutMs || 1800000;
111
+ const timeoutMs = (0, timeout_1.resolveTimeoutMs)('metro-ready', 30 * 60_000, options.readyTimeoutMs);
111
112
  try {
112
113
  while (Date.now() - startedAt < timeoutMs) {
113
114
  if (exitError instanceof errors_1.HarmonyCliError) {
@@ -0,0 +1,8 @@
1
+ declare function timeoutEnvName(operation: string): string;
2
+ /**
3
+ * Resolves a command timeout: explicit option first, then the
4
+ * `EXPO_HARMONY_<OPERATION>_TIMEOUT_MS` environment variable derived from the
5
+ * operation name (dashes become underscores), then the built-in default.
6
+ */
7
+ declare function resolveTimeoutMs(operation: string, fallbackMs: number, explicitMs?: number): number;
8
+ export { resolveTimeoutMs, timeoutEnvName };
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveTimeoutMs = resolveTimeoutMs;
4
+ exports.timeoutEnvName = timeoutEnvName;
5
+ const TimeoutEnvPrefix = 'EXPO_HARMONY_';
6
+ const warnedEnvNames = new Set();
7
+ function timeoutEnvName(operation) {
8
+ return `${TimeoutEnvPrefix}${operation.toUpperCase().replace(/-/gu, '_')}_TIMEOUT_MS`;
9
+ }
10
+ function timeoutFromEnv(operation) {
11
+ const name = timeoutEnvName(operation);
12
+ const raw = process.env[name];
13
+ if (!raw)
14
+ return null;
15
+ const parsed = Number(raw);
16
+ if (Number.isFinite(parsed) && parsed > 0)
17
+ return parsed;
18
+ if (!warnedEnvNames.has(name)) {
19
+ warnedEnvNames.add(name);
20
+ console.warn(`Ignoring unsupported ${name} value: ${raw}. Use a positive number of milliseconds.`);
21
+ }
22
+ return null;
23
+ }
24
+ /**
25
+ * Resolves a command timeout: explicit option first, then the
26
+ * `EXPO_HARMONY_<OPERATION>_TIMEOUT_MS` environment variable derived from the
27
+ * operation name (dashes become underscores), then the built-in default.
28
+ */
29
+ function resolveTimeoutMs(operation, fallbackMs, explicitMs) {
30
+ return explicitMs && explicitMs > 0
31
+ ? explicitMs
32
+ : timeoutFromEnv(operation) ?? fallbackMs;
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo-harmony/cli",
3
- "version": "55.0.26-harmony.11",
3
+ "version": "55.0.26-harmony.12",
4
4
  "keywords": [
5
5
  "react-native",
6
6
  "expo",
@@ -8,6 +8,7 @@ import { verifyModulesAsync } from '@expo-harmony/expo-modules-autolinking';
8
8
  import { isRnohAutolinkingDisabled, validateHarmonySigningConfigFile } from '@expo-harmony/prebuild-config/internal';
9
9
 
10
10
  import { spawnAsync } from '../process';
11
+ import { resolveTimeoutMs } from '../timeout';
11
12
  import { withHarmonyProjectLockAsync } from '../projectLock';
12
13
  import { resolveHarmonyBuildPlanIfPresentAsync } from '../native/project';
13
14
  import { resolveHarmonyToolchain, type HarmonyTool } from '../native/toolchain';
@@ -221,7 +222,7 @@ async function doctorUnlockedAsync(root: string, options: DoctorOptions = {}): P
221
222
  capture: true,
222
223
  cwd: root,
223
224
  operation: `doctor-${id}`,
224
- timeoutMs: 10_000,
225
+ timeoutMs: resolveTimeoutMs('doctor', 10_000),
225
226
  });
226
227
 
227
228
  checks.push(result.code === 0 && !result.timedOut
@@ -10,6 +10,7 @@ import { HarmonyCliError } from '../errors';
10
10
  import { withHarmonyProjectLockAsync } from '../projectLock';
11
11
  import { resolveHarmonyBuildPlanAsync } from '../native/project';
12
12
  import { formatDiagnostics, spawnAsync } from '../process';
13
+ import { resolveTimeoutMs } from '../timeout';
13
14
  import { resolveExpoHermesBuilder } from '../expo';
14
15
  import {
15
16
  assertHermesBundle, assertSourceMap, exportPaths,
@@ -88,7 +89,7 @@ async function exportEmbedUnlockedAsync(
88
89
  },
89
90
  operation: 'expo-export-embed',
90
91
  outputLimit: 4 * 1024 * 1024,
91
- timeoutMs: options.timeoutMs || 10 * 60_000,
92
+ timeoutMs: resolveTimeoutMs('expo-export-embed', 10 * 60_000, options.timeoutMs),
92
93
  });
93
94
 
94
95
  if (result.code !== 0 || result.timedOut) {
@@ -12,6 +12,7 @@ import { progress, type Log } from '../log';
12
12
  import { toPosixPath } from '../path';
13
13
  import { runCheckedAsync } from '../process';
14
14
  import { timedAsync } from '../profile';
15
+ import { resolveTimeoutMs } from '../timeout';
15
16
 
16
17
  export interface NativeBuildResult {
17
18
  export: null | { assetCount: number; bundleSha256: string; sourceMapSha256: string };
@@ -70,7 +71,7 @@ export async function buildNativeAsync(
70
71
  },
71
72
  message: 'Hvigor build',
72
73
  operation: 'hvigor-build',
73
- timeoutMs: 15 * 60_000,
74
+ timeoutMs: resolveTimeoutMs('hvigor-build', 15 * 60_000),
74
75
  }));
75
76
 
76
77
  if (!isNonEmptyRegularFile(plan.expectedHap)) {
@@ -1,6 +1,7 @@
1
1
  import type { HarmonyBuildPlan } from './types';
2
2
  import type { HarmonyToolchain } from './toolchain';
3
3
  import { runCheckedAsync } from '../process';
4
+ import { resolveTimeoutMs } from '../timeout';
4
5
 
5
6
  export async function installHarmonyDependenciesAsync(
6
7
  plan: Pick<HarmonyBuildPlan, 'harmonyRoot'>,
@@ -12,6 +13,6 @@ export async function installHarmonyDependenciesAsync(
12
13
  cwd: plan.harmonyRoot,
13
14
  message: 'OHPM install',
14
15
  operation: 'ohpm-install',
15
- timeoutMs: options.timeoutMs || 5 * 60_000,
16
+ timeoutMs: resolveTimeoutMs('ohpm-install', 30 * 60_000, options.timeoutMs),
16
17
  });
17
18
  }
@@ -4,6 +4,7 @@ import { listEmulatorsAsync, startEmulator } from './emulators';
4
4
  import { HarmonyCliError } from '../errors';
5
5
  import { type HarmonyTool } from '../native/toolchain';
6
6
  import { formatDiagnostics, spawnAsync, type ProcessResult } from '../process';
7
+ import { resolveTimeoutMs } from '../timeout';
7
8
 
8
9
  interface Device {
9
10
  aliases: string[];
@@ -74,7 +75,7 @@ async function runHdcAsync(
74
75
  cwd: options.cwd,
75
76
  operation: options.operation || 'hdc',
76
77
  outputLimit: options.outputLimit || 256 * 1024,
77
- timeoutMs: options.timeoutMs || 60_000,
78
+ timeoutMs: resolveTimeoutMs(options.operation || 'hdc', 60_000, options.timeoutMs),
78
79
  });
79
80
 
80
81
  if (!options.allowFailure && hasCommandFailure(result)) {
@@ -95,7 +96,7 @@ async function listConnectedDevicesAsync(hdc: HarmonyTool, options: HdcOptions):
95
96
  cwd: options.cwd,
96
97
  message: 'Cannot list Harmony devices',
97
98
  operation: 'list-devices',
98
- timeoutMs: options.timeoutMs || 15_000,
99
+ timeoutMs: resolveTimeoutMs('list-devices', 15_000, options.timeoutMs),
99
100
  });
100
101
  const targets = parseHdcTargets(result.stdout || result.stderr);
101
102
  return targets.filter(target => target.state.toLowerCase() === 'connected');
@@ -173,7 +174,7 @@ async function selectDeviceAsync(
173
174
  }
174
175
 
175
176
  options.onProgress?.(`Waiting for Harmony emulator ${instance.name} to connect`);
176
- const deadline = Date.now() + (options.timeoutMs || 120_000);
177
+ const deadline = Date.now() + resolveTimeoutMs('start-emulator', 120_000, options.timeoutMs);
177
178
  while (Date.now() < deadline) {
178
179
  launch?.assertRunning();
179
180
  const devices = await listConnectedDevicesAsync(hdc, {
@@ -223,7 +224,7 @@ async function installHapAsync(
223
224
  cwd: options.cwd,
224
225
  message: `Cannot install the Harmony HAP on ${device.id}`,
225
226
  operation: 'install-hap',
226
- timeoutMs: options.timeoutMs || 2 * 60_000,
227
+ timeoutMs: resolveTimeoutMs('install-hap', 2 * 60_000, options.timeoutMs),
227
228
  });
228
229
  }
229
230
 
@@ -247,7 +248,7 @@ async function configureMetroPortAsync(
247
248
  const forwards = await runHdcAsync(hdc, ['-t', device.id, 'fport', 'ls'], {
248
249
  cwd: options.cwd,
249
250
  operation: 'list-metro-ports',
250
- timeoutMs: 15_000,
251
+ timeoutMs: resolveTimeoutMs('list-metro-ports', 15_000, options.timeoutMs),
251
252
  });
252
253
 
253
254
  for (const line of forwards.stdout.split(/\r?\n/u)) {
@@ -258,7 +259,7 @@ async function configureMetroPortAsync(
258
259
  await runHdcAsync(hdc, ['-t', device.id, 'fport', 'rm', remote, local], {
259
260
  cwd: options.cwd,
260
261
  operation: 'remove-metro-port',
261
- timeoutMs: 15_000,
262
+ timeoutMs: resolveTimeoutMs('remove-metro-port', 15_000, options.timeoutMs),
262
263
  });
263
264
  }
264
265
 
@@ -267,7 +268,7 @@ async function configureMetroPortAsync(
267
268
  cwd: options.cwd,
268
269
  message: `Cannot reverse Metro port ${port} for ${device.id}`,
269
270
  operation: 'reverse-metro-port',
270
- timeoutMs: 15_000,
271
+ timeoutMs: resolveTimeoutMs('reverse-metro-port', 15_000, options.timeoutMs),
271
272
  });
272
273
  }
273
274
 
@@ -288,7 +289,7 @@ async function launchAppAsync(
288
289
  allowFailure: true,
289
290
  cwd: options.cwd,
290
291
  operation: 'force-stop-app',
291
- timeoutMs: 15_000,
292
+ timeoutMs: resolveTimeoutMs('force-stop-app', 15_000),
292
293
  });
293
294
 
294
295
  await runHdcAsync(hdc, [
@@ -303,7 +304,7 @@ async function launchAppAsync(
303
304
  cwd: options.cwd,
304
305
  message: `Cannot launch ${bundleName} on ${device.id}`,
305
306
  operation: 'launch-app',
306
- timeoutMs: 30_000,
307
+ timeoutMs: resolveTimeoutMs('launch-app', 30_000),
307
308
  });
308
309
  }
309
310
 
@@ -4,6 +4,7 @@ import spawn from 'cross-spawn';
4
4
 
5
5
  import { HarmonyCliError } from '../errors';
6
6
  import { formatDiagnostics, spawnAsync } from '../process';
7
+ import { resolveTimeoutMs } from '../timeout';
7
8
  import { type HarmonyTool } from '../native/toolchain';
8
9
 
9
10
  interface HarmonyEmulator {
@@ -21,7 +22,7 @@ async function listEmulatorsAsync(
21
22
  capture: true,
22
23
  cwd: options.cwd,
23
24
  operation: 'list-emulators',
24
- timeoutMs: options.timeoutMs || 15_000,
25
+ timeoutMs: resolveTimeoutMs('list-emulators', 15_000, options.timeoutMs),
25
26
  });
26
27
  } catch (cause) {
27
28
  throw new HarmonyCliError(
package/src/run/metro.ts CHANGED
@@ -4,6 +4,7 @@ import { readDevelopmentSessionAsync } from '../development/session';
4
4
  import { HarmonyCliError } from '../errors';
5
5
  import { formatDiagnostics, startManagedProcess, type ProcessResult } from '../process';
6
6
  import { resolveExpoCli } from '../expo';
7
+ import { resolveTimeoutMs } from '../timeout';
7
8
 
8
9
  type MetroStatus = 'free' | 'metro' | 'occupied';
9
10
 
@@ -155,7 +156,7 @@ async function startExpoMetroAsync(
155
156
  );
156
157
 
157
158
  const startedAt = Date.now();
158
- const timeoutMs = options.readyTimeoutMs || 1800000;
159
+ const timeoutMs = resolveTimeoutMs('metro-ready', 30 * 60_000, options.readyTimeoutMs);
159
160
 
160
161
  try {
161
162
  while (Date.now() - startedAt < timeoutMs) {
package/src/timeout.ts ADDED
@@ -0,0 +1,35 @@
1
+ const TimeoutEnvPrefix = 'EXPO_HARMONY_';
2
+
3
+ const warnedEnvNames = new Set<string>();
4
+
5
+ function timeoutEnvName(operation: string): string {
6
+ return `${TimeoutEnvPrefix}${operation.toUpperCase().replace(/-/gu, '_')}_TIMEOUT_MS`;
7
+ }
8
+
9
+ function timeoutFromEnv(operation: string): number | null {
10
+ const name = timeoutEnvName(operation);
11
+ const raw = process.env[name];
12
+ if (!raw) return null;
13
+
14
+ const parsed = Number(raw);
15
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
16
+
17
+ if (!warnedEnvNames.has(name)) {
18
+ warnedEnvNames.add(name);
19
+ console.warn(`Ignoring unsupported ${name} value: ${raw}. Use a positive number of milliseconds.`);
20
+ }
21
+ return null;
22
+ }
23
+
24
+ /**
25
+ * Resolves a command timeout: explicit option first, then the
26
+ * `EXPO_HARMONY_<OPERATION>_TIMEOUT_MS` environment variable derived from the
27
+ * operation name (dashes become underscores), then the built-in default.
28
+ */
29
+ function resolveTimeoutMs(operation: string, fallbackMs: number, explicitMs?: number): number {
30
+ return explicitMs && explicitMs > 0
31
+ ? explicitMs
32
+ : timeoutFromEnv(operation) ?? fallbackMs;
33
+ }
34
+
35
+ export { resolveTimeoutMs, timeoutEnvName };