@expo-harmony/cli 55.0.26-harmony.11 → 55.0.26-harmony.13
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 +32 -5
- package/build/development/host.d.ts +2 -0
- package/build/development/host.js +50 -0
- package/build/development/session.js +2 -7
- package/build/doctor/doctor.d.ts +1 -0
- package/build/doctor/doctor.js +6 -2
- package/build/exportEmbed/export.js +2 -1
- package/build/help.d.ts +1 -1
- package/build/help.js +1 -1
- package/build/native/build.js +2 -1
- package/build/native/install.js +2 -1
- package/build/prebuild/index.js +1 -0
- package/build/prebuild/prebuild.js +5 -0
- package/build/run/devices.js +10 -9
- package/build/run/emulators.js +2 -1
- package/build/run/metro.js +7 -4
- package/build/run/options.d.ts +1 -0
- package/build/run/options.js +3 -0
- package/build/run/run.d.ts +1 -0
- package/build/run/run.js +2 -0
- package/build/runtime/contract.js +0 -4
- package/build/start/options.js +2 -4
- package/build/timeout.d.ts +8 -0
- package/build/timeout.js +33 -0
- package/package.json +2 -2
- package/src/development/host.ts +62 -0
- package/src/development/session.ts +2 -11
- package/src/doctor/doctor.ts +17 -3
- package/src/exportEmbed/export.ts +2 -1
- package/src/help.ts +1 -1
- package/src/native/build.ts +2 -1
- package/src/native/install.ts +2 -1
- package/src/prebuild/index.ts +1 -0
- package/src/prebuild/prebuild.ts +7 -0
- package/src/run/devices.ts +10 -9
- package/src/run/emulators.ts +2 -1
- package/src/run/metro.ts +7 -4
- package/src/run/options.ts +3 -0
- package/src/run/run.ts +5 -2
- package/src/runtime/contract.ts +0 -9
- package/src/start/options.ts +2 -9
- package/src/timeout.ts +35 -0
package/README.md
CHANGED
|
@@ -49,6 +49,7 @@ npx expo-harmony run ./my-app --device <hdc-target>
|
|
|
49
49
|
```sh
|
|
50
50
|
npx expo-harmony start
|
|
51
51
|
npx expo-harmony start --port 8082
|
|
52
|
+
npx expo-harmony start --host 192.168.1.100
|
|
52
53
|
npx expo-harmony start --clear
|
|
53
54
|
```
|
|
54
55
|
|
|
@@ -56,6 +57,8 @@ npx expo-harmony start --clear
|
|
|
56
57
|
|
|
57
58
|
可以用 `--port <number>` 指定端口,默认是 `8081`。`--reset-cache` 清除 Metro 缓存,也可以写成 `--clear` 或 `-c`。
|
|
58
59
|
|
|
60
|
+
多网卡环境下,可以用 `--host <IP 或主机名>` 指定设备连接的电脑地址,例如 `npx expo-harmony start --host 192.168.1.100 --port 8082`。地址应是设备能访问的网卡 IPv4 或主机名,不带协议和端口。
|
|
61
|
+
|
|
59
62
|
端口上已有 Metro 时,命令会提示并退出,已有服务继续运行。要清除它的缓存,需要先停止服务,再带缓存选项启动。端口被其他进程占用时,命令会报错。
|
|
60
63
|
|
|
61
64
|
要构建并启动应用,在另一个终端运行 `npx expo-harmony run --no-bundler`。如果指定了端口,两个命令应使用相同的 `--port`。
|
|
@@ -80,7 +83,7 @@ npx expo-harmony prebuild --check
|
|
|
80
83
|
|
|
81
84
|
`--clean` 删除原生工程后重新生成。如果工程缺少 CNG 清单或模板标记,或者目录是符号链接、位于项目之外,命令会拒绝删除。
|
|
82
85
|
|
|
83
|
-
`--check` 比较现有文件与当前配置生成的文件,不修改项目文件。没有差异时退出码为 0
|
|
86
|
+
`--check` 比较现有文件与当前配置生成的文件,不修改项目文件。没有差异时退出码为 0,有差异时列出变更并以退出码 2 结束。这个选项不能和会修改工程的选项一起使用。
|
|
84
87
|
|
|
85
88
|
## Build
|
|
86
89
|
|
|
@@ -94,7 +97,7 @@ npx expo-harmony build --sync
|
|
|
94
97
|
|
|
95
98
|
默认构建 Debug 版本。使用 `--variant release` 构建 Release 版本时,命令会将 JS Bundle 和资源打包进应用。
|
|
96
99
|
|
|
97
|
-
项目没有原生工程时,命令会先运行 prebuild。已有 CNG
|
|
100
|
+
项目没有原生工程时,命令会先运行 prebuild。已有 CNG 工程时,会检查生成的文件是否需要更新,有差异就停止构建,可以用 `--sync` 重新生成后继续构建。bare 工程按现有原生配置构建,不支持 `--sync`。
|
|
98
101
|
|
|
99
102
|
## Doctor
|
|
100
103
|
|
|
@@ -113,7 +116,7 @@ npx expo-harmony doctor
|
|
|
113
116
|
- HDC、OHPM、Hvigor 是否可用
|
|
114
117
|
- 原生工程的 Hvigor 文件和自动链接配置
|
|
115
118
|
|
|
116
|
-
有 `error` 级别的问题时,命令以非零退出码结束。CNG 工程未配置外部签名文件时会显示警告,仍可构建未签名的 HAP
|
|
119
|
+
有 `error` 级别的问题时,命令以非零退出码结束。CNG 工程未配置外部签名文件时会显示警告,仍可构建未签名的 HAP。首次生成、删除 `harmony/` 后重新生成或使用 `prebuild --clean` 时,若配置的签名文件或材料缺失、无效,会给出警告并按未签名生成;签名配置可用时仍会正常应用。对已有工程执行普通 prebuild 或 doctor 时,签名文件缺失或无效会被视为 `error`。证书是否有效,需要在构建时由 Hvigor 检查。
|
|
117
120
|
|
|
118
121
|
## Modules
|
|
119
122
|
|
|
@@ -154,13 +157,14 @@ npx expo-harmony run \
|
|
|
154
157
|
--port 8081
|
|
155
158
|
```
|
|
156
159
|
|
|
157
|
-
`run` 构建 HAP
|
|
160
|
+
`run` 构建 HAP,在设备或模拟器上安装并启动应用。它使用项目中已有的原生工程,工程不存在时先运行 prebuild。
|
|
158
161
|
|
|
159
162
|
常用选项:
|
|
160
163
|
|
|
161
164
|
- `--variant debug|release`:构建模式,默认 `debug`。Release 将 JS Bundle 和资源打包进应用,不启动 Metro。
|
|
162
165
|
- `--device <id-or-name>`:选择已连接的 HDC 设备,或按完整名称启动本地模拟器,例如 `--device "Pura 90 Pro"`。有多个候选目标时必须指定。
|
|
163
166
|
- `--port <number>`:电脑上的 Metro 端口,默认 `8081`。Debug 模式下会配置设备端口映射,让应用连接到这个端口。
|
|
167
|
+
- `--host <IP 或主机名>`:指定本次启动的 Metro 对外公布的地址,与 `start --host` 相同;使用 `--no-bundler` 时应在启动 Metro 的命令中指定。
|
|
164
168
|
- `--no-bundler`:使用已运行的 Metro,找不到服务时报错。不加此选项时,会启动 Metro,或复用端口上已有的 Metro。
|
|
165
169
|
- `--reset-cache`:Debug 模式下启动 Metro 时清除 Metro 缓存;Release 模式下清除生产导出缓存。
|
|
166
170
|
- `--no-install`:仍会构建 HAP,但跳过安装,启动设备上已有的应用。
|
|
@@ -169,7 +173,7 @@ npx expo-harmony run \
|
|
|
169
173
|
|
|
170
174
|
Debug 模式下,如果 Metro 由这个命令启动,日志会显示在当前终端,按 Ctrl+C 退出。
|
|
171
175
|
|
|
172
|
-
未指定 `--device`
|
|
176
|
+
未指定 `--device` 时,优先使用已连接的设备。没有连接的设备时,先等待正在启动的模拟器,没有的话就启动本地唯一的模拟器。有多个可选设备或模拟器时,需要用 `--device` 指定。
|
|
173
177
|
|
|
174
178
|
按名称选择或自动启动模拟器时,CLI 会等待模拟器开机并连接 HDC,最多等待 120 秒。退出 CLI 或 Metro 后,模拟器继续运行。
|
|
175
179
|
|
|
@@ -183,6 +187,7 @@ CLI 先读取环境变量指定的工具路径,再查找 DevEco Studio 的安
|
|
|
183
187
|
|
|
184
188
|
| 环境变量 | 用途 |
|
|
185
189
|
| --- | --- |
|
|
190
|
+
| `REACT_NATIVE_PACKAGER_HOSTNAME` | 指定 Metro 对外公布的 IP 或主机名,`--host` 优先 |
|
|
186
191
|
| `HARMONY_HDC` | 指定 HDC 路径 |
|
|
187
192
|
| `HARMONY_EMULATOR` | 指定模拟器命令行工具路径 |
|
|
188
193
|
| `HARMONY_OHPM` | 指定 OHPM 路径 |
|
|
@@ -191,6 +196,28 @@ CLI 先读取环境变量指定的工具路径,再查找 DevEco Studio 的安
|
|
|
191
196
|
| `EXPO_HARMONY_NODE` | 指定原生构建过程中运行 Expo Harmony CLI 的 Node.js 路径,要求 Node.js 20 或更高版本 |
|
|
192
197
|
| `DEVECO_SDK_HOME`、`HARMONY_HOME` 或 `OHOS_SDK_HOME` | 指定 HarmonyOS SDK 根目录 |
|
|
193
198
|
|
|
199
|
+
### 超时
|
|
200
|
+
|
|
201
|
+
OHPM 安装、Hvigor 构建、等待模拟器这些步骤都有超时,默认值见下表,可以通过环境变量覆盖。变量名为 `EXPO_HARMONY_<操作名大写下划线>_TIMEOUT_MS`,单位为毫秒,例如 `EXPO_HARMONY_OHPM_INSTALL_TIMEOUT_MS`。设置成 0、负数或无法解析的值时,这个变量会被忽略,并给出警告。
|
|
202
|
+
|
|
203
|
+
| 环境变量 | 对应操作 | 默认值 |
|
|
204
|
+
| --- | --- | --- |
|
|
205
|
+
| `EXPO_HARMONY_OHPM_INSTALL_TIMEOUT_MS` | OHPM 依赖安装 | 30 分钟 |
|
|
206
|
+
| `EXPO_HARMONY_HVIGOR_BUILD_TIMEOUT_MS` | Hvigor 构建 | 15 分钟 |
|
|
207
|
+
| `EXPO_HARMONY_EXPO_EXPORT_EMBED_TIMEOUT_MS` | `export:embed`(Hermes 打包) | 10 分钟 |
|
|
208
|
+
| `EXPO_HARMONY_METRO_READY_TIMEOUT_MS` | 等待 Metro 就绪 | 30 分钟 |
|
|
209
|
+
| `EXPO_HARMONY_START_EMULATOR_TIMEOUT_MS` | 等待模拟器连接 HDC | 2 分钟 |
|
|
210
|
+
| `EXPO_HARMONY_INSTALL_HAP_TIMEOUT_MS` | 安装 HAP 到设备 | 2 分钟 |
|
|
211
|
+
| `EXPO_HARMONY_LAUNCH_APP_TIMEOUT_MS` | 启动应用 | 30 秒 |
|
|
212
|
+
| `EXPO_HARMONY_LIST_DEVICES_TIMEOUT_MS` | HDC 列出设备 | 15 秒 |
|
|
213
|
+
| `EXPO_HARMONY_LIST_EMULATORS_TIMEOUT_MS` | 列出模拟器实例 | 15 秒 |
|
|
214
|
+
| `EXPO_HARMONY_FORCE_STOP_APP_TIMEOUT_MS` | 强制停止应用 | 15 秒 |
|
|
215
|
+
| `EXPO_HARMONY_LIST_METRO_PORTS_TIMEOUT_MS` | 列出 Metro 端口转发 | 15 秒 |
|
|
216
|
+
| `EXPO_HARMONY_REMOVE_METRO_PORT_TIMEOUT_MS` | 移除 Metro 端口转发 | 15 秒 |
|
|
217
|
+
| `EXPO_HARMONY_REVERSE_METRO_PORT_TIMEOUT_MS` | 反向转发 Metro 端口 | 15 秒 |
|
|
218
|
+
| `EXPO_HARMONY_HDC_TIMEOUT_MS` | 其他 HDC 命令 | 60 秒 |
|
|
219
|
+
| `EXPO_HARMONY_DOCTOR_TIMEOUT_MS` | Doctor 工具检查 | 10 秒 |
|
|
220
|
+
|
|
194
221
|
## Author
|
|
195
222
|
|
|
196
223
|
**expo-harmony** © [Baoshuo](https://github.com/renbaoshuo), Released under the MIT License.<br>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseHostOption = parseHostOption;
|
|
7
|
+
exports.resolveDevelopmentHostAsync = resolveDevelopmentHostAsync;
|
|
8
|
+
const node_module_1 = require("node:module");
|
|
9
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
10
|
+
const errors_1 = require("../errors");
|
|
11
|
+
const expo_1 = require("../expo");
|
|
12
|
+
function parseHostOption(host) {
|
|
13
|
+
if (host !== undefined && !/^[A-Za-z0-9.-]+$/.test(host)) {
|
|
14
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--host must be a hostname or IPv4 address without a port or scheme.', { operation: 'parse-arguments' });
|
|
15
|
+
}
|
|
16
|
+
return host;
|
|
17
|
+
}
|
|
18
|
+
function interfacePriority(name, info) {
|
|
19
|
+
// Interface names alone cannot identify every virtual adapter (especially on Windows).
|
|
20
|
+
const virtual = /^(utun|tun|tap|wg|ppp|ipsec|vmnet|vboxnet|virbr|docker|br-|bridge\d|veth|tailscale|zt)|virtual|vmware|vpn/i.test(name)
|
|
21
|
+
|| /^(00:00:00:00:00:00|00:05:69:|00:0c:29:|00:1c:14:|00:50:56:|08:00:27:|00:15:5d:)/i.test(info.mac)
|
|
22
|
+
|| /^198\.(18|19)\./.test(info.address);
|
|
23
|
+
return Number(virtual) + (info.address.startsWith('169.254.') ? 2 : 0);
|
|
24
|
+
}
|
|
25
|
+
async function resolveDevelopmentHostAsync(root, hostname) {
|
|
26
|
+
const override = hostname || process.env.REACT_NATIVE_PACKAGER_HOSTNAME;
|
|
27
|
+
if (override) {
|
|
28
|
+
if (!/^[A-Za-z0-9.-]+$/.test(override)) {
|
|
29
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_MANIFEST_HOST', 'Harmony development requires an HTTP LAN hostname or IPv4 address.', { operation: 'development-manifest' });
|
|
30
|
+
}
|
|
31
|
+
return override;
|
|
32
|
+
}
|
|
33
|
+
const candidates = Object.entries(node_os_1.default.networkInterfaces()).flatMap(([name, addresses]) => (addresses || [])
|
|
34
|
+
.filter(info => info.family === 'IPv4' && !info.internal)
|
|
35
|
+
.map(info => ({ address: info.address, priority: interfacePriority(name, info) }))).sort((a, b) => a.priority - b.priority);
|
|
36
|
+
if (!candidates.length)
|
|
37
|
+
return '127.0.0.1';
|
|
38
|
+
const preferred = candidates.filter(candidate => candidate.priority === candidates[0].priority);
|
|
39
|
+
if (preferred.length > 1) {
|
|
40
|
+
// Use Expo's route discovery to choose between equally suitable interfaces,
|
|
41
|
+
// but do not let a VPN default route displace an available LAN interface.
|
|
42
|
+
const expo = (0, node_module_1.createRequire)((0, expo_1.resolveExpoCli)(root).cliPath);
|
|
43
|
+
const require = (0, node_module_1.createRequire)(expo.resolve('@expo/cli/package.json'));
|
|
44
|
+
const { getGatewayAsync } = require('./build/src/utils/ip');
|
|
45
|
+
const gateway = await getGatewayAsync();
|
|
46
|
+
if (preferred.some(candidate => candidate.address === gateway.address))
|
|
47
|
+
return gateway.address;
|
|
48
|
+
}
|
|
49
|
+
return preferred[0].address;
|
|
50
|
+
}
|
|
@@ -5,19 +5,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.readDevelopmentSessionAsync = readDevelopmentSessionAsync;
|
|
7
7
|
const node_http_1 = __importDefault(require("node:http"));
|
|
8
|
-
const node_os_1 = __importDefault(require("node:os"));
|
|
9
8
|
const errors_1 = require("../errors");
|
|
10
9
|
const config_1 = require("../runtime/config");
|
|
10
|
+
const host_1 = require("./host");
|
|
11
11
|
const protocol_1 = require("./protocol");
|
|
12
12
|
async function readDevelopmentSessionAsync(root, port, hostname) {
|
|
13
13
|
const project = await (0, config_1.readProjectRuntimeAsync)(root);
|
|
14
14
|
const { config } = project;
|
|
15
|
-
const host =
|
|
16
|
-
|| Object.values(node_os_1.default.networkInterfaces()).flat().find(item => item?.family === 'IPv4' && !item.internal)?.address
|
|
17
|
-
|| '127.0.0.1';
|
|
18
|
-
if (!/^[A-Za-z0-9.-]+$/.test(host)) {
|
|
19
|
-
throw new errors_1.HarmonyCliError('ERR_HARMONY_MANIFEST_HOST', 'Harmony development requires an HTTP LAN hostname or IPv4 address.', { operation: 'development-manifest' });
|
|
20
|
-
}
|
|
15
|
+
const host = await (0, host_1.resolveDevelopmentHostAsync)(root, hostname);
|
|
21
16
|
const manifest = await new Promise((resolve, reject) => {
|
|
22
17
|
const request = node_http_1.default.get({
|
|
23
18
|
host: '127.0.0.1',
|
package/build/doctor/doctor.d.ts
CHANGED
package/build/doctor/doctor.js
CHANGED
|
@@ -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");
|
|
@@ -115,7 +116,10 @@ async function doctorUnlockedAsync(root, options = {}) {
|
|
|
115
116
|
checks.push(check('signing', 'pass', `Harmony signing config ${signing.name} is valid.`));
|
|
116
117
|
}
|
|
117
118
|
catch (error) {
|
|
118
|
-
|
|
119
|
+
const fresh = options.freshPrebuild || !node_fs_1.default.existsSync(node_path_1.default.join(root, internal_1.HarmonyPlatformDirectory));
|
|
120
|
+
checks.push(check('signing', fresh ? 'warn' : 'error', fresh
|
|
121
|
+
? `The fresh Harmony project will be generated unsigned because signing materials are unavailable: ${error.message}`
|
|
122
|
+
: error.message, { code: error.code || 'ERR_HARMONY_SIGNING_INVALID' }));
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
else {
|
|
@@ -179,7 +183,7 @@ async function doctorUnlockedAsync(root, options = {}) {
|
|
|
179
183
|
capture: true,
|
|
180
184
|
cwd: root,
|
|
181
185
|
operation: `doctor-${id}`,
|
|
182
|
-
timeoutMs: 10_000,
|
|
186
|
+
timeoutMs: (0, timeout_1.resolveTimeoutMs)('doctor', 10_000),
|
|
183
187
|
});
|
|
184
188
|
checks.push(result.code === 0 && !result.timedOut
|
|
185
189
|
? 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:
|
|
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);
|
package/build/help.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const Help = "Usage: expo-harmony <command> [project] [options]\n\nCommands:\n start Start Expo Metro for Harmony development\n runtime Describe project requirements; --client file checks compatibility\n runtime --publish Embed capabilities from prepared native build inputs\n prebuild Generate the Harmony native project with Expo CNG\n prebuild --clean Safely recreate the managed Harmony directory\n prebuild --check Compare generated desired state without project writes\n build Build a HAP without selecting or contacting a device\n doctor Validate config, versions, Metro, RNOH, SDK, and signing\n modules list List native module candidates and their discovery source\n modules inspect Resolve Harmony metadata (--package narrows the result)\n modules verify Validate module config, registration conflicts and safe paths\n export Export an OTA update, assets, metadata, and source map\n export:embed Export validated Hermes bytecode, assets, and a source map\n run Build, install, and launch the Harmony app\n\nOptions:\n --no-install Skip dependency install (prebuild) or HAP install (run)\n --npm|--yarn|--pnpm|--bun\n Select the package manager used by prebuild dependency install\n --skip-dependency-update <packages>\n Preserve comma-separated dependency versions\n --device <id-or-name> Select an HDC target or start a local emulator by name\n --variant <mode> Build debug or release (default: debug)\n --no-bundler Use an already-running Expo Metro server\n --app-id <bundleName>\n Launch another installed app (requires --no-install when different)\n --host <hostname> Advertised
|
|
1
|
+
export declare const Help = "Usage: expo-harmony <command> [project] [options]\n\nCommands:\n start Start Expo Metro for Harmony development\n runtime Describe project requirements; --client file checks compatibility\n runtime --publish Embed capabilities from prepared native build inputs\n prebuild Generate the Harmony native project with Expo CNG\n prebuild --clean Safely recreate the managed Harmony directory\n prebuild --check Compare generated desired state without project writes\n build Build a HAP without selecting or contacting a device\n doctor Validate config, versions, Metro, RNOH, SDK, and signing\n modules list List native module candidates and their discovery source\n modules inspect Resolve Harmony metadata (--package narrows the result)\n modules verify Validate module config, registration conflicts and safe paths\n export Export an OTA update, assets, metadata, and source map\n export:embed Export validated Hermes bytecode, assets, and a source map\n run Build, install, and launch the Harmony app\n\nOptions:\n --no-install Skip dependency install (prebuild) or HAP install (run)\n --npm|--yarn|--pnpm|--bun\n Select the package manager used by prebuild dependency install\n --skip-dependency-update <packages>\n Preserve comma-separated dependency versions\n --device <id-or-name> Select an HDC target or start a local emulator by name\n --variant <mode> Build debug or release (default: debug)\n --no-bundler Use an already-running Expo Metro server\n --app-id <bundleName>\n Launch another installed app (requires --no-install when different)\n --host <hostname> Advertised LAN hostname or IPv4 address (start, run)\n --private-key-path <path>\n Sign development manifests using the configured Updates certificate\n --port <number> Metro and device reverse port (default: 8081)\n --sync Re-run prebuild before building\n --output-dir <path> OTA export directory (default: dist)\n --asset-url <url> HTTP(S) root hosting exported OTA assets\n --check Validate an existing export without writing\n --reset-cache Reset Metro while exporting or starting\n -c, --clear Alias for --reset-cache (start)\n -h, --help Show this help\n";
|
package/build/help.js
CHANGED
|
@@ -30,7 +30,7 @@ Options:
|
|
|
30
30
|
--no-bundler Use an already-running Expo Metro server
|
|
31
31
|
--app-id <bundleName>
|
|
32
32
|
Launch another installed app (requires --no-install when different)
|
|
33
|
-
--host <hostname> Advertised
|
|
33
|
+
--host <hostname> Advertised LAN hostname or IPv4 address (start, run)
|
|
34
34
|
--private-key-path <path>
|
|
35
35
|
Sign development manifests using the configured Updates certificate
|
|
36
36
|
--port <number> Metro and device reverse port (default: 8081)
|
package/build/native/build.js
CHANGED
|
@@ -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' });
|
package/build/native/install.js
CHANGED
|
@@ -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:
|
|
12
|
+
timeoutMs: (0, timeout_1.resolveTimeoutMs)('ohpm-install', 30 * 60_000, options.timeoutMs),
|
|
12
13
|
});
|
|
13
14
|
}
|
package/build/prebuild/index.js
CHANGED
|
@@ -21,6 +21,7 @@ const command = async (argv, io) => {
|
|
|
21
21
|
}
|
|
22
22
|
const { doctorAsync, formatDoctor } = await import('../doctor/doctor.js');
|
|
23
23
|
const doctor = await doctorAsync(root, {
|
|
24
|
+
freshPrebuild: clean,
|
|
24
25
|
requireBuildTools: false,
|
|
25
26
|
validateGeneratedProject: !clean,
|
|
26
27
|
validateModules: false,
|
|
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.prebuildParsedAsync = prebuildParsedAsync;
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const internal_1 = require("@expo-harmony/prebuild-config/internal");
|
|
8
10
|
const errors_1 = require("../errors");
|
|
9
11
|
const bare_1 = require("../native/bare");
|
|
10
12
|
const process_1 = require("../process");
|
|
@@ -20,6 +22,8 @@ async function prebuildParsedUnlockedAsync(projectRoot, passthrough, options = {
|
|
|
20
22
|
}
|
|
21
23
|
if (passthrough.includes('--clean'))
|
|
22
24
|
await (0, clean_1.assertSafeCleanTarget)(projectRoot);
|
|
25
|
+
const freshHarmonyProject = passthrough.includes('--clean')
|
|
26
|
+
|| !node_fs_1.default.existsSync(node_path_1.default.join(projectRoot, internal_1.HarmonyPlatformDirectory));
|
|
23
27
|
const expo = (0, expo_1.resolveExpoCli)(projectRoot);
|
|
24
28
|
const packed = await (0, template_1.packAsync)(projectRoot);
|
|
25
29
|
try {
|
|
@@ -35,6 +39,7 @@ async function prebuildParsedUnlockedAsync(projectRoot, passthrough, options = {
|
|
|
35
39
|
env: {
|
|
36
40
|
...(0, toolchain_1.createHarmonyToolchainEnv)(),
|
|
37
41
|
...packed.env,
|
|
42
|
+
EXPO_HARMONY_PREBUILD_FRESH: freshHarmonyProject ? '1' : '0',
|
|
38
43
|
...(options.buildType ? { EXPO_HARMONY_BUILD_TYPE: options.buildType } : {}),
|
|
39
44
|
},
|
|
40
45
|
operation: 'expo-prebuild',
|
package/build/run/devices.js
CHANGED
|
@@ -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.
|
|
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:
|
|
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
|
|
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:
|
|
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
|
}
|
package/build/run/emulators.js
CHANGED
|
@@ -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:
|
|
21
|
+
timeoutMs: (0, timeout_1.resolveTimeoutMs)('list-emulators', 15_000, options.timeoutMs),
|
|
21
22
|
});
|
|
22
23
|
}
|
|
23
24
|
catch (cause) {
|
package/build/run/metro.js
CHANGED
|
@@ -6,10 +6,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.requireExistingMetroAsync = requireExistingMetroAsync;
|
|
7
7
|
exports.startExpoMetroAsync = startExpoMetroAsync;
|
|
8
8
|
const node_http_1 = __importDefault(require("node:http"));
|
|
9
|
+
const host_1 = require("../development/host");
|
|
9
10
|
const session_1 = require("../development/session");
|
|
10
11
|
const errors_1 = require("../errors");
|
|
11
12
|
const process_1 = require("../process");
|
|
12
13
|
const expo_1 = require("../expo");
|
|
14
|
+
const timeout_1 = require("../timeout");
|
|
13
15
|
function delay(milliseconds) {
|
|
14
16
|
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
15
17
|
}
|
|
@@ -65,10 +67,11 @@ async function requireExistingMetroAsync(port) {
|
|
|
65
67
|
throw new errors_1.HarmonyCliError(code, message, { operation: 'metro-probe' });
|
|
66
68
|
}
|
|
67
69
|
async function startExpoMetroAsync(projectRoot, options) {
|
|
70
|
+
const host = await (0, host_1.resolveDevelopmentHostAsync)(projectRoot, options.host);
|
|
68
71
|
const before = await probeMetroAsync(options.port);
|
|
69
72
|
if (before === 'metro') {
|
|
70
73
|
return {
|
|
71
|
-
development: await (0, session_1.readDevelopmentSessionAsync)(projectRoot, options.port,
|
|
74
|
+
development: await (0, session_1.readDevelopmentSessionAsync)(projectRoot, options.port, host),
|
|
72
75
|
owner: 'existing',
|
|
73
76
|
port: options.port,
|
|
74
77
|
stop: async () => { },
|
|
@@ -93,7 +96,7 @@ async function startExpoMetroAsync(projectRoot, options) {
|
|
|
93
96
|
env: {
|
|
94
97
|
...process.env,
|
|
95
98
|
EXPO_METRO_TARGET: 'harmony',
|
|
96
|
-
|
|
99
|
+
REACT_NATIVE_PACKAGER_HOSTNAME: host,
|
|
97
100
|
},
|
|
98
101
|
operation: 'expo-metro',
|
|
99
102
|
outputLimit: 1024 * 1024,
|
|
@@ -107,7 +110,7 @@ async function startExpoMetroAsync(projectRoot, options) {
|
|
|
107
110
|
exitError = error;
|
|
108
111
|
});
|
|
109
112
|
const startedAt = Date.now();
|
|
110
|
-
const timeoutMs = options.readyTimeoutMs
|
|
113
|
+
const timeoutMs = (0, timeout_1.resolveTimeoutMs)('metro-ready', 30 * 60_000, options.readyTimeoutMs);
|
|
111
114
|
try {
|
|
112
115
|
while (Date.now() - startedAt < timeoutMs) {
|
|
113
116
|
if (exitError instanceof errors_1.HarmonyCliError) {
|
|
@@ -127,7 +130,7 @@ async function startExpoMetroAsync(projectRoot, options) {
|
|
|
127
130
|
}
|
|
128
131
|
if (await probeMetroAsync(options.port) === 'metro') {
|
|
129
132
|
return {
|
|
130
|
-
development: await (0, session_1.readDevelopmentSessionAsync)(projectRoot, options.port,
|
|
133
|
+
development: await (0, session_1.readDevelopmentSessionAsync)(projectRoot, options.port, host),
|
|
131
134
|
owner: 'started',
|
|
132
135
|
port: options.port,
|
|
133
136
|
process: managed,
|
package/build/run/options.d.ts
CHANGED
package/build/run/options.js
CHANGED
|
@@ -3,10 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.parseRunArgs = parseRunArgs;
|
|
4
4
|
const errors_1 = require("../errors");
|
|
5
5
|
const args_1 = require("../args");
|
|
6
|
+
const host_1 = require("../development/host");
|
|
6
7
|
const RunOptions = {
|
|
7
8
|
...args_1.CommonOptions,
|
|
8
9
|
'app-id': { type: 'string' },
|
|
9
10
|
'device': { type: 'string' },
|
|
11
|
+
'host': { type: 'string' },
|
|
10
12
|
'no-bundler': { type: 'boolean' },
|
|
11
13
|
'no-install': { type: 'boolean' },
|
|
12
14
|
'port': { type: 'string' },
|
|
@@ -50,6 +52,7 @@ function parseRunArgs(argv) {
|
|
|
50
52
|
appId,
|
|
51
53
|
device,
|
|
52
54
|
help: Boolean(values.help),
|
|
55
|
+
host: (0, host_1.parseHostOption)(values.host),
|
|
53
56
|
noBundler: Boolean(values['no-bundler']),
|
|
54
57
|
noInstall: Boolean(values['no-install']),
|
|
55
58
|
port,
|
package/build/run/run.d.ts
CHANGED
package/build/run/run.js
CHANGED
|
@@ -33,6 +33,7 @@ async function runHarmonyUnlockedAsync(root, options = {}) {
|
|
|
33
33
|
const settings = {
|
|
34
34
|
appId: options.appId,
|
|
35
35
|
device: options.device,
|
|
36
|
+
host: options.host,
|
|
36
37
|
interactiveBundler: Boolean(options.interactiveBundler),
|
|
37
38
|
io: options.io || console,
|
|
38
39
|
noBundler: Boolean(options.noBundler),
|
|
@@ -72,6 +73,7 @@ async function runHarmonyUnlockedAsync(root, options = {}) {
|
|
|
72
73
|
metro = await (0, profile_1.timedAsync)(steps, 'metro', () => settings.noBundler
|
|
73
74
|
? (0, metro_1.requireExistingMetroAsync)(settings.port)
|
|
74
75
|
: (0, metro_1.startExpoMetroAsync)(root, {
|
|
76
|
+
host: settings.host,
|
|
75
77
|
interactive: settings.interactiveBundler,
|
|
76
78
|
port: settings.port,
|
|
77
79
|
privateKeyPath: settings.privateKeyPath,
|
|
@@ -40,10 +40,6 @@ async function createRuntimeContractAsync(root, project, modules, metro) {
|
|
|
40
40
|
modules: (0, expo_modules_autolinking_1.createNativeModuleContracts)(modules),
|
|
41
41
|
config: project.native,
|
|
42
42
|
};
|
|
43
|
-
if (contract.rnoh !== '0.84.1' || contract.react !== '19.2.3' || contract.hermes !== '250829098.0.9'
|
|
44
|
-
|| !contract.expo.startsWith('55.') || !modules.some(module => module.packageName === '@expo-harmony/expo-modules-core')) {
|
|
45
|
-
throw new errors_1.HarmonyCliError('ERR_HARMONY_RUNTIME_UNSUPPORTED', 'The fixed development runtime requires Expo SDK 55, Harmony Expo Modules Core, RNOH 0.84.1, React 19.2.3, and Hermes 250829098.0.9. Check the Metro React alias.', { operation: 'runtime-contract' });
|
|
46
|
-
}
|
|
47
43
|
const issues = (0, runtime_1.validateHarmonyRuntime)(contract);
|
|
48
44
|
if (issues.length > 0) {
|
|
49
45
|
throw new errors_1.HarmonyCliError('ERR_HARMONY_RUNTIME_CONFIG', issues.map(issue => issue.message).join('\n'), { operation: 'runtime-contract' });
|
package/build/start/options.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.parseStartArgs = parseStartArgs;
|
|
4
4
|
const args_1 = require("../args");
|
|
5
|
+
const host_1 = require("../development/host");
|
|
5
6
|
const errors_1 = require("../errors");
|
|
6
7
|
const StartOptions = {
|
|
7
8
|
...args_1.CommonOptions,
|
|
@@ -24,12 +25,9 @@ function parseStartArgs(argv) {
|
|
|
24
25
|
operation: 'parse-arguments',
|
|
25
26
|
});
|
|
26
27
|
}
|
|
27
|
-
if (values.host !== undefined && !/^[A-Za-z0-9.-]+$/.test(values.host)) {
|
|
28
|
-
throw new errors_1.HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--host must be a hostname or IPv4 address without a port or scheme.', { operation: 'parse-arguments' });
|
|
29
|
-
}
|
|
30
28
|
return {
|
|
31
29
|
help: Boolean(values.help),
|
|
32
|
-
host: values.host,
|
|
30
|
+
host: (0, host_1.parseHostOption)(values.host),
|
|
33
31
|
port,
|
|
34
32
|
privateKeyPath: values['private-key-path'],
|
|
35
33
|
project: positionals[0],
|
|
@@ -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 };
|
package/build/timeout.js
ADDED
|
@@ -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.
|
|
3
|
+
"version": "55.0.26-harmony.13",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"react-native",
|
|
6
6
|
"expo",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@expo-harmony/config-plugins": "55.0.10-harmony.5",
|
|
55
55
|
"@expo-harmony/expo-modules-autolinking": "55.0.25-harmony.5",
|
|
56
|
-
"@expo-harmony/prebuild-config": "55.0.0-harmony.
|
|
56
|
+
"@expo-harmony/prebuild-config": "55.0.0-harmony.10",
|
|
57
57
|
"@expo/config": "55.0.17",
|
|
58
58
|
"@expo/fingerprint": "0.16.7",
|
|
59
59
|
"cross-spawn": "^7.0.6",
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
|
|
4
|
+
import { HarmonyCliError } from '../errors';
|
|
5
|
+
import { resolveExpoCli } from '../expo';
|
|
6
|
+
|
|
7
|
+
export function parseHostOption(host?: string): string | undefined {
|
|
8
|
+
if (host !== undefined && !/^[A-Za-z0-9.-]+$/.test(host)) {
|
|
9
|
+
throw new HarmonyCliError(
|
|
10
|
+
'ERR_HARMONY_CONFIG_INVALID',
|
|
11
|
+
'--host must be a hostname or IPv4 address without a port or scheme.',
|
|
12
|
+
{ operation: 'parse-arguments' }
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return host;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function interfacePriority(name: string, info: os.NetworkInterfaceInfo): number {
|
|
20
|
+
// Interface names alone cannot identify every virtual adapter (especially on Windows).
|
|
21
|
+
const virtual = /^(utun|tun|tap|wg|ppp|ipsec|vmnet|vboxnet|virbr|docker|br-|bridge\d|veth|tailscale|zt)|virtual|vmware|vpn/i.test(name)
|
|
22
|
+
|| /^(00:00:00:00:00:00|00:05:69:|00:0c:29:|00:1c:14:|00:50:56:|08:00:27:|00:15:5d:)/i.test(info.mac)
|
|
23
|
+
|| /^198\.(18|19)\./.test(info.address);
|
|
24
|
+
|
|
25
|
+
return Number(virtual) + (info.address.startsWith('169.254.') ? 2 : 0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function resolveDevelopmentHostAsync(root: string, hostname?: string): Promise<string> {
|
|
29
|
+
const override = hostname || process.env.REACT_NATIVE_PACKAGER_HOSTNAME;
|
|
30
|
+
if (override) {
|
|
31
|
+
if (!/^[A-Za-z0-9.-]+$/.test(override)) {
|
|
32
|
+
throw new HarmonyCliError(
|
|
33
|
+
'ERR_HARMONY_MANIFEST_HOST',
|
|
34
|
+
'Harmony development requires an HTTP LAN hostname or IPv4 address.',
|
|
35
|
+
{ operation: 'development-manifest' }
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return override;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const candidates = Object.entries(os.networkInterfaces()).flatMap(([name, addresses]) =>
|
|
43
|
+
(addresses || [])
|
|
44
|
+
.filter(info => info.family === 'IPv4' && !info.internal)
|
|
45
|
+
.map(info => ({ address: info.address, priority: interfacePriority(name, info) }))
|
|
46
|
+
).sort((a, b) => a.priority - b.priority);
|
|
47
|
+
|
|
48
|
+
if (!candidates.length) return '127.0.0.1';
|
|
49
|
+
|
|
50
|
+
const preferred = candidates.filter(candidate => candidate.priority === candidates[0].priority);
|
|
51
|
+
if (preferred.length > 1) {
|
|
52
|
+
// Use Expo's route discovery to choose between equally suitable interfaces,
|
|
53
|
+
// but do not let a VPN default route displace an available LAN interface.
|
|
54
|
+
const expo = createRequire(resolveExpoCli(root).cliPath);
|
|
55
|
+
const require = createRequire(expo.resolve('@expo/cli/package.json'));
|
|
56
|
+
const { getGatewayAsync } = require('./build/src/utils/ip');
|
|
57
|
+
const gateway = await getGatewayAsync();
|
|
58
|
+
if (preferred.some(candidate => candidate.address === gateway.address)) return gateway.address;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return preferred[0].address;
|
|
62
|
+
}
|
|
@@ -1,26 +1,17 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
|
-
import os from 'node:os';
|
|
3
2
|
|
|
4
3
|
import type { HarmonyDevelopmentManifest } from '@expo-harmony/expo-modules-autolinking/runtime';
|
|
5
4
|
|
|
6
5
|
import { HarmonyCliError } from '../errors';
|
|
7
6
|
import { readProjectRuntimeAsync } from '../runtime/config';
|
|
7
|
+
import { resolveDevelopmentHostAsync } from './host';
|
|
8
8
|
import { createHarmonyLaunchLink, HarmonyManifestPath } from './protocol';
|
|
9
9
|
|
|
10
10
|
export async function readDevelopmentSessionAsync(root: string, port: number, hostname?: string) {
|
|
11
11
|
const project = await readProjectRuntimeAsync(root);
|
|
12
12
|
const { config } = project;
|
|
13
13
|
|
|
14
|
-
const host =
|
|
15
|
-
|| Object.values(os.networkInterfaces()).flat().find(item => item?.family === 'IPv4' && !item.internal)?.address
|
|
16
|
-
|| '127.0.0.1';
|
|
17
|
-
if (!/^[A-Za-z0-9.-]+$/.test(host)) {
|
|
18
|
-
throw new HarmonyCliError(
|
|
19
|
-
'ERR_HARMONY_MANIFEST_HOST',
|
|
20
|
-
'Harmony development requires an HTTP LAN hostname or IPv4 address.',
|
|
21
|
-
{ operation: 'development-manifest' }
|
|
22
|
-
);
|
|
23
|
-
}
|
|
14
|
+
const host = await resolveDevelopmentHostAsync(root, hostname);
|
|
24
15
|
|
|
25
16
|
const manifest = await new Promise<HarmonyDevelopmentManifest>((resolve, reject) => {
|
|
26
17
|
const request = http.get({
|
package/src/doctor/doctor.ts
CHANGED
|
@@ -5,9 +5,14 @@ import path from 'node:path';
|
|
|
5
5
|
import { getConfig } from '@expo/config';
|
|
6
6
|
import { normalizeHarmonyConfig } from '@expo-harmony/config-plugins';
|
|
7
7
|
import { verifyModulesAsync } from '@expo-harmony/expo-modules-autolinking';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
HarmonyPlatformDirectory,
|
|
10
|
+
isRnohAutolinkingDisabled,
|
|
11
|
+
validateHarmonySigningConfigFile,
|
|
12
|
+
} from '@expo-harmony/prebuild-config/internal';
|
|
9
13
|
|
|
10
14
|
import { spawnAsync } from '../process';
|
|
15
|
+
import { resolveTimeoutMs } from '../timeout';
|
|
11
16
|
import { withHarmonyProjectLockAsync } from '../projectLock';
|
|
12
17
|
import { resolveHarmonyBuildPlanIfPresentAsync } from '../native/project';
|
|
13
18
|
import { resolveHarmonyToolchain, type HarmonyTool } from '../native/toolchain';
|
|
@@ -28,6 +33,7 @@ export interface DoctorResult {
|
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
interface DoctorOptions {
|
|
36
|
+
freshPrebuild?: boolean;
|
|
31
37
|
requireBuildTools?: boolean;
|
|
32
38
|
requireDeviceTools?: boolean;
|
|
33
39
|
validateGeneratedProject?: boolean;
|
|
@@ -139,7 +145,15 @@ async function doctorUnlockedAsync(root: string, options: DoctorOptions = {}): P
|
|
|
139
145
|
const signing = await validateHarmonySigningConfigFile(root, harmony.signingConfigFile);
|
|
140
146
|
checks.push(check('signing', 'pass', `Harmony signing config ${signing.name} is valid.`));
|
|
141
147
|
} catch (error) {
|
|
142
|
-
|
|
148
|
+
const fresh = options.freshPrebuild || !fs.existsSync(path.join(root, HarmonyPlatformDirectory));
|
|
149
|
+
checks.push(check(
|
|
150
|
+
'signing',
|
|
151
|
+
fresh ? 'warn' : 'error',
|
|
152
|
+
fresh
|
|
153
|
+
? `The fresh Harmony project will be generated unsigned because signing materials are unavailable: ${error.message}`
|
|
154
|
+
: error.message,
|
|
155
|
+
{ code: error.code || 'ERR_HARMONY_SIGNING_INVALID' }
|
|
156
|
+
));
|
|
143
157
|
}
|
|
144
158
|
} else {
|
|
145
159
|
checks.push(check('signing', 'warn', 'No external signing config is set; unsigned generation remains available.'));
|
|
@@ -221,7 +235,7 @@ async function doctorUnlockedAsync(root: string, options: DoctorOptions = {}): P
|
|
|
221
235
|
capture: true,
|
|
222
236
|
cwd: root,
|
|
223
237
|
operation: `doctor-${id}`,
|
|
224
|
-
timeoutMs: 10_000,
|
|
238
|
+
timeoutMs: resolveTimeoutMs('doctor', 10_000),
|
|
225
239
|
});
|
|
226
240
|
|
|
227
241
|
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:
|
|
92
|
+
timeoutMs: resolveTimeoutMs('expo-export-embed', 10 * 60_000, options.timeoutMs),
|
|
92
93
|
});
|
|
93
94
|
|
|
94
95
|
if (result.code !== 0 || result.timedOut) {
|
package/src/help.ts
CHANGED
|
@@ -27,7 +27,7 @@ Options:
|
|
|
27
27
|
--no-bundler Use an already-running Expo Metro server
|
|
28
28
|
--app-id <bundleName>
|
|
29
29
|
Launch another installed app (requires --no-install when different)
|
|
30
|
-
--host <hostname> Advertised
|
|
30
|
+
--host <hostname> Advertised LAN hostname or IPv4 address (start, run)
|
|
31
31
|
--private-key-path <path>
|
|
32
32
|
Sign development manifests using the configured Updates certificate
|
|
33
33
|
--port <number> Metro and device reverse port (default: 8081)
|
package/src/native/build.ts
CHANGED
|
@@ -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)) {
|
package/src/native/install.ts
CHANGED
|
@@ -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:
|
|
16
|
+
timeoutMs: resolveTimeoutMs('ohpm-install', 30 * 60_000, options.timeoutMs),
|
|
16
17
|
});
|
|
17
18
|
}
|
package/src/prebuild/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ export const command: Command = async (argv, io) => {
|
|
|
25
25
|
|
|
26
26
|
const { doctorAsync, formatDoctor } = await import('../doctor/doctor.js');
|
|
27
27
|
const doctor = await doctorAsync(root, {
|
|
28
|
+
freshPrebuild: clean,
|
|
28
29
|
requireBuildTools: false,
|
|
29
30
|
validateGeneratedProject: !clean,
|
|
30
31
|
validateModules: false,
|
package/src/prebuild/prebuild.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { HarmonyPlatformDirectory } from '@expo-harmony/prebuild-config/internal';
|
|
2
5
|
|
|
3
6
|
import { HarmonyCliError } from '../errors';
|
|
4
7
|
import { isBareHarmonyProject } from '../native/bare';
|
|
@@ -25,6 +28,9 @@ async function prebuildParsedUnlockedAsync(
|
|
|
25
28
|
}
|
|
26
29
|
if (passthrough.includes('--clean')) await assertSafeCleanTarget(projectRoot);
|
|
27
30
|
|
|
31
|
+
const freshHarmonyProject = passthrough.includes('--clean')
|
|
32
|
+
|| !fs.existsSync(path.join(projectRoot, HarmonyPlatformDirectory));
|
|
33
|
+
|
|
28
34
|
const expo = resolveExpoCli(projectRoot);
|
|
29
35
|
const packed = await packAsync(projectRoot);
|
|
30
36
|
|
|
@@ -41,6 +47,7 @@ async function prebuildParsedUnlockedAsync(
|
|
|
41
47
|
env: {
|
|
42
48
|
...createHarmonyToolchainEnv(),
|
|
43
49
|
...packed.env,
|
|
50
|
+
EXPO_HARMONY_PREBUILD_FRESH: freshHarmonyProject ? '1' : '0',
|
|
44
51
|
...(options.buildType ? { EXPO_HARMONY_BUILD_TYPE: options.buildType } : {}),
|
|
45
52
|
},
|
|
46
53
|
operation: 'expo-prebuild',
|
package/src/run/devices.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
package/src/run/emulators.ts
CHANGED
|
@@ -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
|
|
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
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
|
|
3
|
+
import { resolveDevelopmentHostAsync } from '../development/host';
|
|
3
4
|
import { readDevelopmentSessionAsync } from '../development/session';
|
|
4
5
|
import { HarmonyCliError } from '../errors';
|
|
5
6
|
import { formatDiagnostics, startManagedProcess, type ProcessResult } from '../process';
|
|
6
7
|
import { resolveExpoCli } from '../expo';
|
|
8
|
+
import { resolveTimeoutMs } from '../timeout';
|
|
7
9
|
|
|
8
10
|
type MetroStatus = 'free' | 'metro' | 'occupied';
|
|
9
11
|
|
|
@@ -101,11 +103,12 @@ async function startExpoMetroAsync(
|
|
|
101
103
|
projectRoot: string,
|
|
102
104
|
options: MetroOptions
|
|
103
105
|
): Promise<MetroSession> {
|
|
106
|
+
const host = await resolveDevelopmentHostAsync(projectRoot, options.host);
|
|
104
107
|
const before = await probeMetroAsync(options.port);
|
|
105
108
|
|
|
106
109
|
if (before === 'metro') {
|
|
107
110
|
return {
|
|
108
|
-
development: await readDevelopmentSessionAsync(projectRoot, options.port,
|
|
111
|
+
development: await readDevelopmentSessionAsync(projectRoot, options.port, host),
|
|
109
112
|
owner: 'existing',
|
|
110
113
|
port: options.port,
|
|
111
114
|
stop: async () => {},
|
|
@@ -136,7 +139,7 @@ async function startExpoMetroAsync(
|
|
|
136
139
|
env: {
|
|
137
140
|
...process.env,
|
|
138
141
|
EXPO_METRO_TARGET: 'harmony',
|
|
139
|
-
|
|
142
|
+
REACT_NATIVE_PACKAGER_HOSTNAME: host,
|
|
140
143
|
},
|
|
141
144
|
operation: 'expo-metro',
|
|
142
145
|
outputLimit: 1024 * 1024,
|
|
@@ -155,7 +158,7 @@ async function startExpoMetroAsync(
|
|
|
155
158
|
);
|
|
156
159
|
|
|
157
160
|
const startedAt = Date.now();
|
|
158
|
-
const timeoutMs = options.readyTimeoutMs
|
|
161
|
+
const timeoutMs = resolveTimeoutMs('metro-ready', 30 * 60_000, options.readyTimeoutMs);
|
|
159
162
|
|
|
160
163
|
try {
|
|
161
164
|
while (Date.now() - startedAt < timeoutMs) {
|
|
@@ -192,7 +195,7 @@ async function startExpoMetroAsync(
|
|
|
192
195
|
|
|
193
196
|
if (await probeMetroAsync(options.port) === 'metro') {
|
|
194
197
|
return {
|
|
195
|
-
development: await readDevelopmentSessionAsync(projectRoot, options.port,
|
|
198
|
+
development: await readDevelopmentSessionAsync(projectRoot, options.port, host),
|
|
196
199
|
owner: 'started',
|
|
197
200
|
port: options.port,
|
|
198
201
|
process: managed,
|
package/src/run/options.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { HarmonyCliError } from '../errors';
|
|
2
2
|
import { CommonOptions, parseArgs } from '../args';
|
|
3
|
+
import { parseHostOption } from '../development/host';
|
|
3
4
|
|
|
4
5
|
const RunOptions = {
|
|
5
6
|
...CommonOptions,
|
|
6
7
|
'app-id': { type: 'string' },
|
|
7
8
|
'device': { type: 'string' },
|
|
9
|
+
'host': { type: 'string' },
|
|
8
10
|
'no-bundler': { type: 'boolean' },
|
|
9
11
|
'no-install': { type: 'boolean' },
|
|
10
12
|
'port': { type: 'string' },
|
|
@@ -59,6 +61,7 @@ function parseRunArgs(argv: string[]) {
|
|
|
59
61
|
appId,
|
|
60
62
|
device,
|
|
61
63
|
help: Boolean(values.help),
|
|
64
|
+
host: parseHostOption(values.host),
|
|
62
65
|
noBundler: Boolean(values['no-bundler']),
|
|
63
66
|
noInstall: Boolean(values['no-install']),
|
|
64
67
|
port,
|
package/src/run/run.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { timedAsync } from '../profile';
|
|
|
24
24
|
export interface HarmonyRunOptions {
|
|
25
25
|
appId?: string;
|
|
26
26
|
device?: string;
|
|
27
|
+
host?: string;
|
|
27
28
|
io?: Pick<Console, 'error' | 'log' | 'warn'>;
|
|
28
29
|
noBundler?: boolean;
|
|
29
30
|
noInstall?: boolean;
|
|
@@ -60,8 +61,8 @@ interface HarmonyRunSession {
|
|
|
60
61
|
result: HarmonyRunResult;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
type NormalizedRunOptions = Required<Omit<HarmonyRunSessionOptions, 'appId' | 'device' | 'privateKeyPath'>>
|
|
64
|
-
& Pick<HarmonyRunSessionOptions, 'appId' | 'device' | 'privateKeyPath'>;
|
|
64
|
+
type NormalizedRunOptions = Required<Omit<HarmonyRunSessionOptions, 'appId' | 'device' | 'host' | 'privateKeyPath'>>
|
|
65
|
+
& Pick<HarmonyRunSessionOptions, 'appId' | 'device' | 'host' | 'privateKeyPath'>;
|
|
65
66
|
|
|
66
67
|
const BundleName = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*){2,}$/u;
|
|
67
68
|
|
|
@@ -91,6 +92,7 @@ async function runHarmonyUnlockedAsync(
|
|
|
91
92
|
const settings: NormalizedRunOptions = {
|
|
92
93
|
appId: options.appId,
|
|
93
94
|
device: options.device,
|
|
95
|
+
host: options.host,
|
|
94
96
|
interactiveBundler: Boolean(options.interactiveBundler),
|
|
95
97
|
io: options.io || console,
|
|
96
98
|
noBundler: Boolean(options.noBundler),
|
|
@@ -140,6 +142,7 @@ async function runHarmonyUnlockedAsync(
|
|
|
140
142
|
metro = await timedAsync(steps, 'metro', () => settings.noBundler
|
|
141
143
|
? requireExistingMetroAsync(settings.port)
|
|
142
144
|
: startExpoMetroAsync(root, {
|
|
145
|
+
host: settings.host,
|
|
143
146
|
interactive: settings.interactiveBundler,
|
|
144
147
|
port: settings.port,
|
|
145
148
|
privateKeyPath: settings.privateKeyPath,
|
package/src/runtime/contract.ts
CHANGED
|
@@ -49,15 +49,6 @@ async function createRuntimeContractAsync(
|
|
|
49
49
|
config: project.native,
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
-
if (contract.rnoh !== '0.84.1' || contract.react !== '19.2.3' || contract.hermes !== '250829098.0.9'
|
|
53
|
-
|| !contract.expo.startsWith('55.') || !modules.some(module => module.packageName === '@expo-harmony/expo-modules-core')) {
|
|
54
|
-
throw new HarmonyCliError(
|
|
55
|
-
'ERR_HARMONY_RUNTIME_UNSUPPORTED',
|
|
56
|
-
'The fixed development runtime requires Expo SDK 55, Harmony Expo Modules Core, RNOH 0.84.1, React 19.2.3, and Hermes 250829098.0.9. Check the Metro React alias.',
|
|
57
|
-
{ operation: 'runtime-contract' }
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
52
|
const issues = validateHarmonyRuntime(contract);
|
|
62
53
|
if (issues.length > 0) {
|
|
63
54
|
throw new HarmonyCliError(
|
package/src/start/options.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CommonOptions, parseArgs } from '../args';
|
|
2
|
+
import { parseHostOption } from '../development/host';
|
|
2
3
|
import { HarmonyCliError } from '../errors';
|
|
3
4
|
|
|
4
5
|
const StartOptions = {
|
|
@@ -27,17 +28,9 @@ function parseStartArgs(argv: string[]) {
|
|
|
27
28
|
});
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
if (values.host !== undefined && !/^[A-Za-z0-9.-]+$/.test(values.host)) {
|
|
31
|
-
throw new HarmonyCliError(
|
|
32
|
-
'ERR_HARMONY_CONFIG_INVALID',
|
|
33
|
-
'--host must be a hostname or IPv4 address without a port or scheme.',
|
|
34
|
-
{ operation: 'parse-arguments' }
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
31
|
return {
|
|
39
32
|
help: Boolean(values.help),
|
|
40
|
-
host: values.host,
|
|
33
|
+
host: parseHostOption(values.host),
|
|
41
34
|
port,
|
|
42
35
|
privateKeyPath: values['private-key-path'],
|
|
43
36
|
project: positionals[0],
|
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 };
|