@expo-harmony/cli 55.0.26-harmony.10 → 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 +27 -5
- package/build/development/manifest.js +23 -2
- package/build/development/protocol.d.ts +1 -0
- package/build/development/protocol.js +12 -5
- package/build/doctor/doctor.js +2 -1
- package/build/exportEmbed/export.js +2 -1
- package/build/native/build.js +2 -1
- package/build/native/install.js +2 -1
- package/build/run/devices.js +17 -9
- package/build/run/emulators.js +2 -1
- package/build/run/metro.js +2 -1
- package/build/runtime/config.d.ts +6 -4
- package/build/runtime/config.js +56 -5
- package/build/runtime/contract.js +2 -1
- package/build/runtime/fingerprint.js +8 -3
- package/build/timeout.d.ts +8 -0
- package/build/timeout.js +33 -0
- package/package.json +4 -4
- package/src/development/manifest.ts +24 -3
- package/src/development/protocol.ts +11 -6
- package/src/doctor/doctor.ts +2 -1
- package/src/exportEmbed/export.ts +2 -1
- package/src/native/build.ts +2 -1
- package/src/native/install.ts +2 -1
- package/src/run/devices.ts +18 -9
- package/src/run/emulators.ts +2 -1
- package/src/run/metro.ts +2 -1
- package/src/runtime/config.ts +64 -7
- package/src/runtime/contract.ts +4 -2
- package/src/runtime/fingerprint.ts +13 -3
- package/src/timeout.ts +35 -0
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ npm install --save-dev @expo-harmony/cli
|
|
|
14
14
|
|
|
15
15
|
通过 Expo 配置生成原生工程的项目(CNG),还需要在 Expo 配置中注册 `@expo-harmony/prebuild-config` 插件。
|
|
16
16
|
|
|
17
|
-
手工维护 `harmony/` 工程的项目可以使用 bare 模式,按原生工程中的配置构建和运行,无需注册 prebuild
|
|
17
|
+
手工维护 `harmony/` 工程的项目可以使用 bare 模式,按原生工程中的配置构建和运行,无需注册 prebuild 插件。
|
|
18
18
|
|
|
19
19
|
构建 HAP 需要 HarmonyOS SDK(含 HMS 和 OpenHarmony 组件)、OHPM 和 Hvigor。连接设备或模拟器还需要 HDC。可以运行 `npx expo-harmony doctor` 检查环境。
|
|
20
20
|
|
|
@@ -80,7 +80,7 @@ npx expo-harmony prebuild --check
|
|
|
80
80
|
|
|
81
81
|
`--clean` 删除原生工程后重新生成。如果工程缺少 CNG 清单或模板标记,或者目录是符号链接、位于项目之外,命令会拒绝删除。
|
|
82
82
|
|
|
83
|
-
`--check` 比较现有文件与当前配置生成的文件,不修改项目文件。没有差异时退出码为 0
|
|
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
|
|
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
|
|
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`
|
|
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>
|
|
@@ -26,7 +26,7 @@ async function createManifestResponseAsync(middleware, options, require) {
|
|
|
26
26
|
bundle.searchParams.set('lazy', 'false');
|
|
27
27
|
const name = exp.harmony?.bundleName || (await (0, project_1.resolveHarmonyBuildPlanAsync)(middleware.projectRoot)).bundleName;
|
|
28
28
|
const id = exp.extra?.eas?.projectId;
|
|
29
|
-
const address = `${bundle.origin}${protocol_1.HarmonyManifestPath}?platform=harmony
|
|
29
|
+
const address = (0, protocol_1.canonicalHarmonyManifestURL)(`${bundle.origin}${options.harmonyManifestPath ?? `${protocol_1.HarmonyManifestPath}?platform=harmony`}`);
|
|
30
30
|
const manifest = JSON.stringify({
|
|
31
31
|
id: node_crypto_1.default.randomUUID(),
|
|
32
32
|
createdAt: new Date().toISOString(),
|
|
@@ -77,6 +77,23 @@ function installHarmonyManifest(root) {
|
|
|
77
77
|
const { BundlerDevServer } = require('./build/src/start/server/BundlerDevServer');
|
|
78
78
|
const { parsePlatformHeader } = require('./build/src/start/server/middleware/resolvePlatform');
|
|
79
79
|
const create = BundlerDevServer.prototype.getManifestMiddlewareAsync;
|
|
80
|
+
// Reuse Expo's terminal QR renderer and dev-session publishing with our
|
|
81
|
+
// registered link, rather than emitting a second, incompatible QR code.
|
|
82
|
+
const nativeURL = BundlerDevServer.prototype.getNativeRuntimeUrl;
|
|
83
|
+
const redirectURL = BundlerDevServer.prototype.getRedirectUrl;
|
|
84
|
+
BundlerDevServer.prototype.getNativeRuntimeUrl = function (options = {}) {
|
|
85
|
+
const address = this.getUrlCreator().constructUrl({ ...options, scheme: 'http' });
|
|
86
|
+
if (!address)
|
|
87
|
+
return nativeURL.call(this, options);
|
|
88
|
+
const manifest = new URL(protocol_1.HarmonyManifestPath, address);
|
|
89
|
+
manifest.searchParams.set('platform', 'harmony');
|
|
90
|
+
return (0, protocol_1.createHarmonyLaunchLink)(manifest.toString());
|
|
91
|
+
};
|
|
92
|
+
BundlerDevServer.prototype.getRedirectUrl = function (platform = null) {
|
|
93
|
+
return platform === null || platform === 'harmony'
|
|
94
|
+
? this.getNativeRuntimeUrl()
|
|
95
|
+
: redirectURL.call(this, platform);
|
|
96
|
+
};
|
|
80
97
|
// Expo installs the manifest before Metro's enhancer; adapt only the created instance.
|
|
81
98
|
BundlerDevServer.prototype.getManifestMiddlewareAsync = async function (...args) {
|
|
82
99
|
const middleware = await create.apply(this, args);
|
|
@@ -87,10 +104,14 @@ function installHarmonyManifest(root) {
|
|
|
87
104
|
return parse.call(middleware, request);
|
|
88
105
|
// Bypass only the upstream platform assertion without changing the actual request.
|
|
89
106
|
const url = new URL(request.url, 'http://localhost');
|
|
107
|
+
url.searchParams.delete('platform');
|
|
108
|
+
url.searchParams.append('platform', 'harmony');
|
|
109
|
+
const manifest = new URL((0, protocol_1.canonicalHarmonyManifestURL)(url.toString()));
|
|
110
|
+
const harmonyManifestPath = manifest.pathname + manifest.search;
|
|
90
111
|
url.searchParams.set('platform', 'ios');
|
|
91
112
|
const copy = Object.create(request);
|
|
92
113
|
copy.url = url.pathname + url.search;
|
|
93
|
-
return { ...parse.call(middleware, copy), platform: 'harmony' };
|
|
114
|
+
return { ...parse.call(middleware, copy), platform: 'harmony', harmonyManifestPath };
|
|
94
115
|
};
|
|
95
116
|
middleware._getManifestResponseAsync = options => options.platform === 'harmony'
|
|
96
117
|
? createManifestResponseAsync(middleware, options, require)
|
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HarmonyManifestPath = void 0;
|
|
4
|
+
exports.canonicalHarmonyManifestURL = canonicalHarmonyManifestURL;
|
|
4
5
|
exports.createHarmonyLaunchLink = createHarmonyLaunchLink;
|
|
5
6
|
const errors_1 = require("../errors");
|
|
6
7
|
exports.HarmonyManifestPath = '/manifest';
|
|
7
|
-
function
|
|
8
|
+
function canonicalHarmonyManifestURL(value) {
|
|
8
9
|
let url;
|
|
9
10
|
try {
|
|
10
11
|
url = new URL(value);
|
|
12
|
+
decodeURIComponent(url.search);
|
|
11
13
|
}
|
|
12
14
|
catch (cause) {
|
|
13
15
|
throw new errors_1.HarmonyCliError('ERR_HARMONY_MANIFEST_URL', 'Expected an absolute HTTP(S) Harmony manifest URL.', { cause, operation: 'development-manifest' });
|
|
14
16
|
}
|
|
15
17
|
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash
|
|
16
|
-
|| !['/',
|
|
18
|
+
|| !['/', exports.HarmonyManifestPath, '/index.exp'].includes(url.pathname)
|
|
19
|
+
|| url.searchParams.getAll('platform').length !== 1 || url.searchParams.get('platform') !== 'harmony') {
|
|
17
20
|
throw new errors_1.HarmonyCliError('ERR_HARMONY_MANIFEST_URL', 'Expected an HTTP(S) Expo manifest URL with platform=harmony and without credentials or a fragment.', { operation: 'development-manifest' });
|
|
18
21
|
}
|
|
19
|
-
|
|
22
|
+
url.pathname = exports.HarmonyManifestPath;
|
|
23
|
+
// Keep all other query values (including repetitions) in order. Platform has
|
|
24
|
+
// one canonical position, and URLSearchParams normalizes equivalent encoding.
|
|
25
|
+
url.searchParams.delete('platform');
|
|
26
|
+
url.searchParams.append('platform', 'harmony');
|
|
27
|
+
return url.toString();
|
|
20
28
|
}
|
|
21
29
|
function createHarmonyLaunchLink(manifest) {
|
|
22
|
-
validateManifestUrl(manifest);
|
|
23
30
|
const url = new URL('expo-harmony://open');
|
|
24
|
-
url.searchParams.set('url', manifest);
|
|
31
|
+
url.searchParams.set('url', canonicalHarmonyManifestURL(manifest));
|
|
25
32
|
return url.toString();
|
|
26
33
|
}
|
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");
|
|
@@ -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:
|
|
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/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/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,16 +156,23 @@ 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 = {}) {
|
|
163
|
+
if (options.devicePort === undefined && port !== 8081) {
|
|
164
|
+
// RNOH's default provider uses 8081; launcher manifests retain Metro's
|
|
165
|
+
// advertised port for the bundle, assets, HMR and inspector URLs.
|
|
166
|
+
await configureMetroPortAsync(hdc, device, port, { ...options, devicePort: 8081 });
|
|
167
|
+
await configureMetroPortAsync(hdc, device, port, { ...options, devicePort: port });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
162
170
|
const deviceEndpoint = `tcp:${options.devicePort || 8081}`;
|
|
163
171
|
const hostEndpoint = `tcp:${port}`;
|
|
164
172
|
const forwards = await runHdcAsync(hdc, ['-t', device.id, 'fport', 'ls'], {
|
|
165
173
|
cwd: options.cwd,
|
|
166
174
|
operation: 'list-metro-ports',
|
|
167
|
-
timeoutMs: 15_000,
|
|
175
|
+
timeoutMs: (0, timeout_1.resolveTimeoutMs)('list-metro-ports', 15_000, options.timeoutMs),
|
|
168
176
|
});
|
|
169
177
|
for (const line of forwards.stdout.split(/\r?\n/u)) {
|
|
170
178
|
const [target, remote, local, direction] = line.trim().split(/\s+/u);
|
|
@@ -175,7 +183,7 @@ async function configureMetroPortAsync(hdc, device, port, options = {}) {
|
|
|
175
183
|
await runHdcAsync(hdc, ['-t', device.id, 'fport', 'rm', remote, local], {
|
|
176
184
|
cwd: options.cwd,
|
|
177
185
|
operation: 'remove-metro-port',
|
|
178
|
-
timeoutMs: 15_000,
|
|
186
|
+
timeoutMs: (0, timeout_1.resolveTimeoutMs)('remove-metro-port', 15_000, options.timeoutMs),
|
|
179
187
|
});
|
|
180
188
|
}
|
|
181
189
|
await runHdcAsync(hdc, ['-t', device.id, 'rport', deviceEndpoint, hostEndpoint], {
|
|
@@ -183,7 +191,7 @@ async function configureMetroPortAsync(hdc, device, port, options = {}) {
|
|
|
183
191
|
cwd: options.cwd,
|
|
184
192
|
message: `Cannot reverse Metro port ${port} for ${device.id}`,
|
|
185
193
|
operation: 'reverse-metro-port',
|
|
186
|
-
timeoutMs: 15_000,
|
|
194
|
+
timeoutMs: (0, timeout_1.resolveTimeoutMs)('reverse-metro-port', 15_000, options.timeoutMs),
|
|
187
195
|
});
|
|
188
196
|
}
|
|
189
197
|
async function launchAppAsync(hdc, device, bundleName, abilityName, options = {}) {
|
|
@@ -197,7 +205,7 @@ async function launchAppAsync(hdc, device, bundleName, abilityName, options = {}
|
|
|
197
205
|
allowFailure: true,
|
|
198
206
|
cwd: options.cwd,
|
|
199
207
|
operation: 'force-stop-app',
|
|
200
|
-
timeoutMs: 15_000,
|
|
208
|
+
timeoutMs: (0, timeout_1.resolveTimeoutMs)('force-stop-app', 15_000),
|
|
201
209
|
});
|
|
202
210
|
await runHdcAsync(hdc, [
|
|
203
211
|
'-t', device.id,
|
|
@@ -211,6 +219,6 @@ async function launchAppAsync(hdc, device, bundleName, abilityName, options = {}
|
|
|
211
219
|
cwd: options.cwd,
|
|
212
220
|
message: `Cannot launch ${bundleName} on ${device.id}`,
|
|
213
221
|
operation: 'launch-app',
|
|
214
|
-
timeoutMs: 30_000,
|
|
222
|
+
timeoutMs: (0, timeout_1.resolveTimeoutMs)('launch-app', 30_000),
|
|
215
223
|
});
|
|
216
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
|
@@ -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
|
|
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) {
|
|
@@ -11,8 +11,9 @@ export declare function readNativeRuntime(plan: HarmonyBuildPlan): {
|
|
|
11
11
|
app: any;
|
|
12
12
|
config: {
|
|
13
13
|
nativeCompiler: string;
|
|
14
|
-
targetApiVersion: number;
|
|
15
|
-
compatibleApiVersion: number;
|
|
14
|
+
targetApiVersion: number | string;
|
|
15
|
+
compatibleApiVersion: number | string;
|
|
16
|
+
nativeLibFilterHash?: string;
|
|
16
17
|
permissions: string[];
|
|
17
18
|
querySchemes: string[];
|
|
18
19
|
backgroundModes: string[];
|
|
@@ -24,8 +25,9 @@ export declare function readProjectRuntimeAsync(root: string): Promise<{
|
|
|
24
25
|
bundleName: string;
|
|
25
26
|
native: {
|
|
26
27
|
nativeCompiler: string;
|
|
27
|
-
targetApiVersion: number;
|
|
28
|
-
compatibleApiVersion: number;
|
|
28
|
+
targetApiVersion: number | string;
|
|
29
|
+
compatibleApiVersion: number | string;
|
|
30
|
+
nativeLibFilterHash?: string;
|
|
29
31
|
permissions: string[];
|
|
30
32
|
querySchemes: string[];
|
|
31
33
|
backgroundModes: string[];
|
package/build/runtime/config.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.readMetroRuntimeAsync = readMetroRuntimeAsync;
|
|
|
8
8
|
exports.readNativeRuntime = readNativeRuntime;
|
|
9
9
|
exports.readProjectRuntimeAsync = readProjectRuntimeAsync;
|
|
10
10
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_crypto_1 = require("node:crypto");
|
|
11
12
|
const node_module_1 = require("node:module");
|
|
12
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
13
14
|
const config_1 = require("@expo/config");
|
|
@@ -56,6 +57,55 @@ async function readMetroRuntimeAsync(root) {
|
|
|
56
57
|
}
|
|
57
58
|
return config?.resolver?.resolveRequest?.harmonyRuntime;
|
|
58
59
|
}
|
|
60
|
+
function onlyKeys(value, keys) {
|
|
61
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
62
|
+
&& Object.keys(value).every(key => keys.includes(key));
|
|
63
|
+
}
|
|
64
|
+
function optionalStrings(value) {
|
|
65
|
+
return value === undefined || (Array.isArray(value) && value.every(item => typeof item === 'string' && item.length > 0));
|
|
66
|
+
}
|
|
67
|
+
function supportedReleaseOptions(modes) {
|
|
68
|
+
if (modes === undefined)
|
|
69
|
+
return true;
|
|
70
|
+
if (!Array.isArray(modes) || modes.length > 1)
|
|
71
|
+
return false;
|
|
72
|
+
return modes.every((mode) => {
|
|
73
|
+
if (!onlyKeys(mode, ['name', 'arkOptions', 'nativeLib']) || mode.name !== 'release')
|
|
74
|
+
return false;
|
|
75
|
+
if (mode.nativeLib !== undefined) {
|
|
76
|
+
const symbol = mode.nativeLib?.debugSymbol;
|
|
77
|
+
if (!onlyKeys(mode.nativeLib, ['debugSymbol']) || !onlyKeys(symbol, ['strip', 'exclude'])
|
|
78
|
+
|| typeof symbol.strip !== 'boolean' || !optionalStrings(symbol.exclude))
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (mode.arkOptions !== undefined) {
|
|
82
|
+
const obfuscation = mode.arkOptions?.obfuscation;
|
|
83
|
+
const rules = obfuscation?.ruleOptions;
|
|
84
|
+
if (!onlyKeys(mode.arkOptions, ['obfuscation']) || !onlyKeys(obfuscation, ['ruleOptions'])
|
|
85
|
+
|| !onlyKeys(rules, ['enable', 'files']) || typeof rules.enable !== 'boolean'
|
|
86
|
+
|| !optionalStrings(rules.files))
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function nativeLibFilterHash(filter) {
|
|
93
|
+
if (filter === undefined)
|
|
94
|
+
return undefined;
|
|
95
|
+
if (!onlyKeys(filter, ['excludes', 'pickFirsts', 'pickLasts', 'enableOverride'])
|
|
96
|
+
|| !['excludes', 'pickFirsts', 'pickLasts'].every(key => optionalStrings(filter[key]))
|
|
97
|
+
|| (filter.enableOverride !== undefined && typeof filter.enableOverride !== 'boolean')) {
|
|
98
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_RUNTIME_CONFIG', 'Unsupported nativeLib.filter configuration.', { operation: 'runtime-contract' });
|
|
99
|
+
}
|
|
100
|
+
const values = {};
|
|
101
|
+
for (const key of ['excludes', 'pickFirsts', 'pickLasts']) {
|
|
102
|
+
if (filter[key]?.length)
|
|
103
|
+
values[key] = [...new Set(filter[key])].sort();
|
|
104
|
+
}
|
|
105
|
+
if (filter.enableOverride)
|
|
106
|
+
values['enableOverride'] = true;
|
|
107
|
+
return Object.keys(values).length ? (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(values)).digest('hex') : undefined;
|
|
108
|
+
}
|
|
59
109
|
function nativeConfig(profile, module, build, product, ability) {
|
|
60
110
|
const selected = profile?.app?.products?.find(item => item.name === product);
|
|
61
111
|
const entry = module?.abilities?.find(item => item.name === ability);
|
|
@@ -63,14 +113,15 @@ function nativeConfig(profile, module, build, product, ability) {
|
|
|
63
113
|
|| /(?:USE_HERMES|HERMES_V1_ENABLED)(?::BOOL)?=(?:OFF|FALSE|0)/i.test(build.buildOption?.externalNativeOptions?.arguments || '')
|
|
64
114
|
|| Object.keys(selected.buildOption || {}).some(key => key !== 'nativeCompiler')
|
|
65
115
|
|| profile.app.buildModeSet?.some(mode => mode.buildOption)
|
|
66
|
-
|| build.buildOptionSet
|
|
67
|
-
throw new errors_1.HarmonyCliError('ERR_HARMONY_RUNTIME_CONFIG', 'Runtime contracts require an existing Harmony product and ability, Hermes v1, and
|
|
116
|
+
|| !supportedReleaseOptions(build.buildOptionSet) || build.targets?.some(target => target.buildOption)) {
|
|
117
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_RUNTIME_CONFIG', 'Runtime contracts require an existing Harmony product and ability, Hermes v1, and no per-target or per-mode overrides except release debugSymbol and ArkTS obfuscation rules.', { operation: 'runtime-contract' });
|
|
68
118
|
}
|
|
69
|
-
const
|
|
119
|
+
const hash = nativeLibFilterHash(build.buildOption?.nativeLib?.filter);
|
|
70
120
|
return {
|
|
71
121
|
nativeCompiler: selected.buildOption?.nativeCompiler,
|
|
72
|
-
targetApiVersion:
|
|
73
|
-
compatibleApiVersion:
|
|
122
|
+
targetApiVersion: (0, config_plugins_1.parseHarmonySdkVersion)(selected.targetSdkVersion, 'targetSdkVersion').api,
|
|
123
|
+
compatibleApiVersion: (0, config_plugins_1.parseHarmonySdkVersion)(selected.compatibleSdkVersion, 'compatibleSdkVersion').api,
|
|
124
|
+
...(hash ? { nativeLibFilterHash: hash } : {}),
|
|
74
125
|
permissions: [...new Set((module.requestPermissions || []).map(item => item.name))].sort(),
|
|
75
126
|
querySchemes: [...new Set(module.querySchemes || [])].sort(),
|
|
76
127
|
backgroundModes: [...new Set(entry.backgroundModes || [])].sort(),
|
|
@@ -25,7 +25,7 @@ async function createRuntimeContractAsync(root, project, modules, metro) {
|
|
|
25
25
|
const version = (name) => require(`${name}/package.json`).version;
|
|
26
26
|
const native = (0, node_module_1.createRequire)(require.resolve(resolution.harmonyPackage + '/package.json'));
|
|
27
27
|
const contract = {
|
|
28
|
-
schemaVersion:
|
|
28
|
+
schemaVersion: (0, runtime_1.harmonyRuntimeSchemaVersion)(project.native),
|
|
29
29
|
platform: 'harmony',
|
|
30
30
|
runtimeVersion: project.runtimeVersion,
|
|
31
31
|
development: true,
|
|
@@ -92,6 +92,7 @@ async function publishRuntimeContractAsync(root, plan) {
|
|
|
92
92
|
requirements.development = plan.buildMode === 'debug';
|
|
93
93
|
const contract = {
|
|
94
94
|
...requirements,
|
|
95
|
+
schemaVersion: (0, runtime_1.harmonyRuntimeSchemaVersion)(native.config),
|
|
95
96
|
runtimeVersion: await (0, config_1.resolveRuntimeVersionAsync)(root, project.config, native.app),
|
|
96
97
|
config: native.config,
|
|
97
98
|
};
|
|
@@ -7,6 +7,7 @@ exports.fingerprintHarmonyAsync = fingerprintHarmonyAsync;
|
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const node_module_1 = require("node:module");
|
|
10
|
+
const config_1 = require("@expo/config");
|
|
10
11
|
const fingerprint_1 = require("@expo/fingerprint");
|
|
11
12
|
const expo_modules_autolinking_1 = require("@expo-harmony/expo-modules-autolinking");
|
|
12
13
|
const errors_1 = require("../errors");
|
|
@@ -15,18 +16,22 @@ async function fingerprintHarmonyAsync(root) {
|
|
|
15
16
|
if (!result.valid)
|
|
16
17
|
throw new errors_1.HarmonyCliError('ERR_HARMONY_FINGERPRINT', 'Build and verify Harmony native modules before calculating the runtime fingerprint.', { operation: 'fingerprint' });
|
|
17
18
|
const require = (0, node_module_1.createRequire)(node_path_1.default.join(root, 'package.json'));
|
|
18
|
-
const
|
|
19
|
+
const modules = (0, expo_modules_autolinking_1.createNativeModuleContracts)(result.modules);
|
|
20
|
+
const { exp } = (0, config_1.getConfig)(root, { skipSDKVersionRequirement: true, isModdedConfig: true });
|
|
19
21
|
const generated = [];
|
|
20
22
|
const previous = node_path_1.default.join(root, '.expo/harmony/export-manifest.json');
|
|
21
23
|
if (node_fs_1.default.existsSync(previous)) {
|
|
22
24
|
const manifest = JSON.parse(node_fs_1.default.readFileSync(previous, 'utf8'));
|
|
23
25
|
generated.push(...manifest.assets.map(asset => `**/harmony/**/src/main/resources/rawfile/${asset.path}`));
|
|
24
26
|
}
|
|
25
|
-
const sources = [{ type: 'contents', id: 'harmonyAutolinking', contents: JSON.stringify(
|
|
26
|
-
for (const name of ['@react-native-oh/react-native-harmony', '@expo-harmony/template', '@expo-harmony/config-plugins', '@expo-harmony/prebuild-config']) {
|
|
27
|
+
const sources = [{ type: 'contents', id: 'harmonyAutolinking', contents: JSON.stringify(modules), reasons: ['harmonyAutolinking'] }];
|
|
28
|
+
for (const name of ['@react-native-oh/react-native-harmony', '@expo-harmony/template', '@expo-harmony/config-plugins', '@expo-harmony/prebuild-config', '@expo-harmony/expo-build-properties']) {
|
|
27
29
|
const file = require.resolve(name + '/package.json');
|
|
28
30
|
sources.push({ type: 'dir', filePath: node_path_1.default.relative(root, node_path_1.default.dirname(file)), reasons: ['harmonyNativeToolchain'] });
|
|
29
31
|
}
|
|
32
|
+
for (const file of exp._internal?.harmonyBuildProperties?.release?.obfuscation?.files ?? []) {
|
|
33
|
+
sources.push({ type: 'file', filePath: file, reasons: ['harmonyObfuscationRules'] });
|
|
34
|
+
}
|
|
30
35
|
if (node_fs_1.default.existsSync(node_path_1.default.join(root, 'harmony')))
|
|
31
36
|
sources.push({ type: 'dir', filePath: 'harmony', reasons: ['harmonyNativeProject'] });
|
|
32
37
|
return (0, fingerprint_1.createFingerprintAsync)(root, {
|
|
@@ -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.12",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"react-native",
|
|
6
6
|
"expo",
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"expo-harmony": "build/bin/expo-harmony.js"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@expo-harmony/config-plugins": "55.0.10-harmony.
|
|
55
|
-
"@expo-harmony/expo-modules-autolinking": "55.0.25-harmony.
|
|
56
|
-
"@expo-harmony/prebuild-config": "55.0.0-harmony.
|
|
54
|
+
"@expo-harmony/config-plugins": "55.0.10-harmony.5",
|
|
55
|
+
"@expo-harmony/expo-modules-autolinking": "55.0.25-harmony.5",
|
|
56
|
+
"@expo-harmony/prebuild-config": "55.0.0-harmony.9",
|
|
57
57
|
"@expo/config": "55.0.17",
|
|
58
58
|
"@expo/fingerprint": "0.16.7",
|
|
59
59
|
"cross-spawn": "^7.0.6",
|
|
@@ -5,7 +5,7 @@ import { HarmonyCliError } from '../errors';
|
|
|
5
5
|
import { resolveExpoCli } from '../expo';
|
|
6
6
|
import { resolveHarmonyBuildPlanAsync } from '../native/project';
|
|
7
7
|
import { resolveRuntimeRequirementsAsync } from '../runtime/contract';
|
|
8
|
-
import { createHarmonyLaunchLink, HarmonyManifestPath } from './protocol';
|
|
8
|
+
import { canonicalHarmonyManifestURL, createHarmonyLaunchLink, HarmonyManifestPath } from './protocol';
|
|
9
9
|
|
|
10
10
|
async function createManifestResponseAsync(middleware, options, require: NodeRequire) {
|
|
11
11
|
const { ExpoGoManifestHandlerMiddleware: Manifest, ResponseContentType: Types } = require('./build/src/start/server/middleware/ExpoGoManifestHandlerMiddleware');
|
|
@@ -24,7 +24,7 @@ async function createManifestResponseAsync(middleware, options, require: NodeReq
|
|
|
24
24
|
|
|
25
25
|
const name = exp.harmony?.bundleName || (await resolveHarmonyBuildPlanAsync(middleware.projectRoot)).bundleName;
|
|
26
26
|
const id = exp.extra?.eas?.projectId;
|
|
27
|
-
const address = `${bundle.origin}${HarmonyManifestPath}?platform=harmony
|
|
27
|
+
const address = canonicalHarmonyManifestURL(`${bundle.origin}${options.harmonyManifestPath ?? `${HarmonyManifestPath}?platform=harmony`}`);
|
|
28
28
|
const manifest = JSON.stringify({
|
|
29
29
|
id: crypto.randomUUID(),
|
|
30
30
|
createdAt: new Date().toISOString(),
|
|
@@ -80,6 +80,23 @@ export function installHarmonyManifest(root: string) {
|
|
|
80
80
|
const { parsePlatformHeader } = require('./build/src/start/server/middleware/resolvePlatform');
|
|
81
81
|
const create = BundlerDevServer.prototype.getManifestMiddlewareAsync;
|
|
82
82
|
|
|
83
|
+
// Reuse Expo's terminal QR renderer and dev-session publishing with our
|
|
84
|
+
// registered link, rather than emitting a second, incompatible QR code.
|
|
85
|
+
const nativeURL = BundlerDevServer.prototype.getNativeRuntimeUrl;
|
|
86
|
+
const redirectURL = BundlerDevServer.prototype.getRedirectUrl;
|
|
87
|
+
BundlerDevServer.prototype.getNativeRuntimeUrl = function (options = {}) {
|
|
88
|
+
const address = this.getUrlCreator().constructUrl({ ...options, scheme: 'http' });
|
|
89
|
+
if (!address) return nativeURL.call(this, options);
|
|
90
|
+
const manifest = new URL(HarmonyManifestPath, address);
|
|
91
|
+
manifest.searchParams.set('platform', 'harmony');
|
|
92
|
+
return createHarmonyLaunchLink(manifest.toString());
|
|
93
|
+
};
|
|
94
|
+
BundlerDevServer.prototype.getRedirectUrl = function (platform = null) {
|
|
95
|
+
return platform === null || platform === 'harmony'
|
|
96
|
+
? this.getNativeRuntimeUrl()
|
|
97
|
+
: redirectURL.call(this, platform);
|
|
98
|
+
};
|
|
99
|
+
|
|
83
100
|
// Expo installs the manifest before Metro's enhancer; adapt only the created instance.
|
|
84
101
|
BundlerDevServer.prototype.getManifestMiddlewareAsync = async function (this: object, ...args) {
|
|
85
102
|
const middleware = await create.apply(this, args);
|
|
@@ -91,11 +108,15 @@ export function installHarmonyManifest(root: string) {
|
|
|
91
108
|
|
|
92
109
|
// Bypass only the upstream platform assertion without changing the actual request.
|
|
93
110
|
const url = new URL(request.url, 'http://localhost');
|
|
111
|
+
url.searchParams.delete('platform');
|
|
112
|
+
url.searchParams.append('platform', 'harmony');
|
|
113
|
+
const manifest = new URL(canonicalHarmonyManifestURL(url.toString()));
|
|
114
|
+
const harmonyManifestPath = manifest.pathname + manifest.search;
|
|
94
115
|
url.searchParams.set('platform', 'ios');
|
|
95
116
|
const copy = Object.create(request);
|
|
96
117
|
copy.url = url.pathname + url.search;
|
|
97
118
|
|
|
98
|
-
return { ...parse.call(middleware, copy), platform: 'harmony' };
|
|
119
|
+
return { ...parse.call(middleware, copy), platform: 'harmony', harmonyManifestPath };
|
|
99
120
|
};
|
|
100
121
|
middleware._getManifestResponseAsync = options => options.platform === 'harmony'
|
|
101
122
|
? createManifestResponseAsync(middleware, options, require)
|
|
@@ -2,11 +2,12 @@ import { HarmonyCliError } from '../errors';
|
|
|
2
2
|
|
|
3
3
|
export const HarmonyManifestPath = '/manifest';
|
|
4
4
|
|
|
5
|
-
function
|
|
5
|
+
export function canonicalHarmonyManifestURL(value: string): string {
|
|
6
6
|
let url: URL;
|
|
7
7
|
|
|
8
8
|
try {
|
|
9
9
|
url = new URL(value);
|
|
10
|
+
decodeURIComponent(url.search);
|
|
10
11
|
} catch (cause) {
|
|
11
12
|
throw new HarmonyCliError(
|
|
12
13
|
'ERR_HARMONY_MANIFEST_URL',
|
|
@@ -16,7 +17,8 @@ function validateManifestUrl(value: string): URL {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash
|
|
19
|
-
|| !['/',
|
|
20
|
+
|| !['/', HarmonyManifestPath, '/index.exp'].includes(url.pathname)
|
|
21
|
+
|| url.searchParams.getAll('platform').length !== 1 || url.searchParams.get('platform') !== 'harmony') {
|
|
20
22
|
throw new HarmonyCliError(
|
|
21
23
|
'ERR_HARMONY_MANIFEST_URL',
|
|
22
24
|
'Expected an HTTP(S) Expo manifest URL with platform=harmony and without credentials or a fragment.',
|
|
@@ -24,14 +26,17 @@ function validateManifestUrl(value: string): URL {
|
|
|
24
26
|
);
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
url.pathname = HarmonyManifestPath;
|
|
30
|
+
// Keep all other query values (including repetitions) in order. Platform has
|
|
31
|
+
// one canonical position, and URLSearchParams normalizes equivalent encoding.
|
|
32
|
+
url.searchParams.delete('platform');
|
|
33
|
+
url.searchParams.append('platform', 'harmony');
|
|
34
|
+
return url.toString();
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
export function createHarmonyLaunchLink(manifest: string): string {
|
|
31
|
-
validateManifestUrl(manifest);
|
|
32
|
-
|
|
33
38
|
const url = new URL('expo-harmony://open');
|
|
34
|
-
url.searchParams.set('url', manifest);
|
|
39
|
+
url.searchParams.set('url', canonicalHarmonyManifestURL(manifest));
|
|
35
40
|
|
|
36
41
|
return url.toString();
|
|
37
42
|
}
|
package/src/doctor/doctor.ts
CHANGED
|
@@ -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:
|
|
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/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/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
|
|
|
@@ -233,13 +234,21 @@ async function configureMetroPortAsync(
|
|
|
233
234
|
port: number,
|
|
234
235
|
options: HdcOptions = {}
|
|
235
236
|
): Promise<void> {
|
|
237
|
+
if (options.devicePort === undefined && port !== 8081) {
|
|
238
|
+
// RNOH's default provider uses 8081; launcher manifests retain Metro's
|
|
239
|
+
// advertised port for the bundle, assets, HMR and inspector URLs.
|
|
240
|
+
await configureMetroPortAsync(hdc, device, port, { ...options, devicePort: 8081 });
|
|
241
|
+
await configureMetroPortAsync(hdc, device, port, { ...options, devicePort: port });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
236
245
|
const deviceEndpoint = `tcp:${options.devicePort || 8081}`;
|
|
237
246
|
const hostEndpoint = `tcp:${port}`;
|
|
238
247
|
|
|
239
248
|
const forwards = await runHdcAsync(hdc, ['-t', device.id, 'fport', 'ls'], {
|
|
240
249
|
cwd: options.cwd,
|
|
241
250
|
operation: 'list-metro-ports',
|
|
242
|
-
timeoutMs: 15_000,
|
|
251
|
+
timeoutMs: resolveTimeoutMs('list-metro-ports', 15_000, options.timeoutMs),
|
|
243
252
|
});
|
|
244
253
|
|
|
245
254
|
for (const line of forwards.stdout.split(/\r?\n/u)) {
|
|
@@ -250,7 +259,7 @@ async function configureMetroPortAsync(
|
|
|
250
259
|
await runHdcAsync(hdc, ['-t', device.id, 'fport', 'rm', remote, local], {
|
|
251
260
|
cwd: options.cwd,
|
|
252
261
|
operation: 'remove-metro-port',
|
|
253
|
-
timeoutMs: 15_000,
|
|
262
|
+
timeoutMs: resolveTimeoutMs('remove-metro-port', 15_000, options.timeoutMs),
|
|
254
263
|
});
|
|
255
264
|
}
|
|
256
265
|
|
|
@@ -259,7 +268,7 @@ async function configureMetroPortAsync(
|
|
|
259
268
|
cwd: options.cwd,
|
|
260
269
|
message: `Cannot reverse Metro port ${port} for ${device.id}`,
|
|
261
270
|
operation: 'reverse-metro-port',
|
|
262
|
-
timeoutMs: 15_000,
|
|
271
|
+
timeoutMs: resolveTimeoutMs('reverse-metro-port', 15_000, options.timeoutMs),
|
|
263
272
|
});
|
|
264
273
|
}
|
|
265
274
|
|
|
@@ -280,7 +289,7 @@ async function launchAppAsync(
|
|
|
280
289
|
allowFailure: true,
|
|
281
290
|
cwd: options.cwd,
|
|
282
291
|
operation: 'force-stop-app',
|
|
283
|
-
timeoutMs: 15_000,
|
|
292
|
+
timeoutMs: resolveTimeoutMs('force-stop-app', 15_000),
|
|
284
293
|
});
|
|
285
294
|
|
|
286
295
|
await runHdcAsync(hdc, [
|
|
@@ -295,7 +304,7 @@ async function launchAppAsync(
|
|
|
295
304
|
cwd: options.cwd,
|
|
296
305
|
message: `Cannot launch ${bundleName} on ${device.id}`,
|
|
297
306
|
operation: 'launch-app',
|
|
298
|
-
timeoutMs: 30_000,
|
|
307
|
+
timeoutMs: resolveTimeoutMs('launch-app', 30_000),
|
|
299
308
|
});
|
|
300
309
|
}
|
|
301
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
|
@@ -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
|
|
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/runtime/config.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { createRequire } from 'node:module';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
|
|
5
6
|
import { getConfig } from '@expo/config';
|
|
6
|
-
import { compileHarmonyModsAsync, normalizeHarmonyConfig, type ExpoConfigWithHarmony } from '@expo-harmony/config-plugins';
|
|
7
|
-
import { withHarmonyPrebuildConfig } from '@expo-harmony/prebuild-config';
|
|
7
|
+
import { compileHarmonyModsAsync, normalizeHarmonyConfig, parseHarmonySdkVersion, type ExpoConfigWithHarmony } from '@expo-harmony/config-plugins';
|
|
8
8
|
import type { HarmonyRuntimeContract } from '@expo-harmony/expo-modules-autolinking/runtime';
|
|
9
|
+
import { withHarmonyPrebuildConfig } from '@expo-harmony/prebuild-config';
|
|
9
10
|
import JSON5 from 'json5';
|
|
10
11
|
|
|
11
12
|
import { fingerprintHarmonyAsync } from './fingerprint';
|
|
@@ -72,27 +73,83 @@ export async function readMetroRuntimeAsync(root: string): Promise<HarmonyMetroR
|
|
|
72
73
|
return config?.resolver?.resolveRequest?.harmonyRuntime;
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
function onlyKeys(value, keys: string[]): boolean {
|
|
77
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
78
|
+
&& Object.keys(value).every(key => keys.includes(key));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function optionalStrings(value): boolean {
|
|
82
|
+
return value === undefined || (Array.isArray(value) && value.every(item => typeof item === 'string' && item.length > 0));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function supportedReleaseOptions(modes): boolean {
|
|
86
|
+
if (modes === undefined) return true;
|
|
87
|
+
if (!Array.isArray(modes) || modes.length > 1) return false;
|
|
88
|
+
|
|
89
|
+
return modes.every((mode) => {
|
|
90
|
+
if (!onlyKeys(mode, ['name', 'arkOptions', 'nativeLib']) || mode.name !== 'release') return false;
|
|
91
|
+
|
|
92
|
+
if (mode.nativeLib !== undefined) {
|
|
93
|
+
const symbol = mode.nativeLib?.debugSymbol;
|
|
94
|
+
|
|
95
|
+
if (!onlyKeys(mode.nativeLib, ['debugSymbol']) || !onlyKeys(symbol, ['strip', 'exclude'])
|
|
96
|
+
|| typeof symbol.strip !== 'boolean' || !optionalStrings(symbol.exclude)) return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (mode.arkOptions !== undefined) {
|
|
100
|
+
const obfuscation = mode.arkOptions?.obfuscation;
|
|
101
|
+
const rules = obfuscation?.ruleOptions;
|
|
102
|
+
|
|
103
|
+
if (!onlyKeys(mode.arkOptions, ['obfuscation']) || !onlyKeys(obfuscation, ['ruleOptions'])
|
|
104
|
+
|| !onlyKeys(rules, ['enable', 'files']) || typeof rules.enable !== 'boolean'
|
|
105
|
+
|| !optionalStrings(rules.files)) return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return true;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function nativeLibFilterHash(filter): string | undefined {
|
|
113
|
+
if (filter === undefined) return undefined;
|
|
114
|
+
if (!onlyKeys(filter, ['excludes', 'pickFirsts', 'pickLasts', 'enableOverride'])
|
|
115
|
+
|| !['excludes', 'pickFirsts', 'pickLasts'].every(key => optionalStrings(filter[key]))
|
|
116
|
+
|| (filter.enableOverride !== undefined && typeof filter.enableOverride !== 'boolean')) {
|
|
117
|
+
throw new HarmonyCliError('ERR_HARMONY_RUNTIME_CONFIG', 'Unsupported nativeLib.filter configuration.', { operation: 'runtime-contract' });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const values = {};
|
|
121
|
+
|
|
122
|
+
for (const key of ['excludes', 'pickFirsts', 'pickLasts']) {
|
|
123
|
+
if (filter[key]?.length) values[key] = [...new Set(filter[key])].sort();
|
|
124
|
+
}
|
|
125
|
+
if (filter.enableOverride) values['enableOverride'] = true;
|
|
126
|
+
|
|
127
|
+
return Object.keys(values).length ? createHash('sha256').update(JSON.stringify(values)).digest('hex') : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
75
130
|
function nativeConfig(profile, module, build, product: string, ability: string): HarmonyRuntimeContract['config'] {
|
|
76
131
|
const selected = profile?.app?.products?.find(item => item.name === product);
|
|
77
132
|
const entry = module?.abilities?.find(item => item.name === ability);
|
|
133
|
+
|
|
78
134
|
if (!selected || !entry || !build
|
|
79
135
|
|| /(?:USE_HERMES|HERMES_V1_ENABLED)(?::BOOL)?=(?:OFF|FALSE|0)/i.test(build.buildOption?.externalNativeOptions?.arguments || '')
|
|
80
136
|
|| Object.keys(selected.buildOption || {}).some(key => key !== 'nativeCompiler')
|
|
81
137
|
|| profile.app.buildModeSet?.some(mode => mode.buildOption)
|
|
82
|
-
|| build.buildOptionSet
|
|
138
|
+
|| !supportedReleaseOptions(build.buildOptionSet) || build.targets?.some(target => target.buildOption)) {
|
|
83
139
|
throw new HarmonyCliError(
|
|
84
140
|
'ERR_HARMONY_RUNTIME_CONFIG',
|
|
85
|
-
'Runtime contracts require an existing Harmony product and ability, Hermes v1, and
|
|
141
|
+
'Runtime contracts require an existing Harmony product and ability, Hermes v1, and no per-target or per-mode overrides except release debugSymbol and ArkTS obfuscation rules.',
|
|
86
142
|
{ operation: 'runtime-contract' }
|
|
87
143
|
);
|
|
88
144
|
}
|
|
89
145
|
|
|
90
|
-
const
|
|
146
|
+
const hash = nativeLibFilterHash(build.buildOption?.nativeLib?.filter);
|
|
91
147
|
|
|
92
148
|
return {
|
|
93
149
|
nativeCompiler: selected.buildOption?.nativeCompiler,
|
|
94
|
-
targetApiVersion:
|
|
95
|
-
compatibleApiVersion:
|
|
150
|
+
targetApiVersion: parseHarmonySdkVersion(selected.targetSdkVersion, 'targetSdkVersion').api,
|
|
151
|
+
compatibleApiVersion: parseHarmonySdkVersion(selected.compatibleSdkVersion, 'compatibleSdkVersion').api,
|
|
152
|
+
...(hash ? { nativeLibFilterHash: hash } : {}),
|
|
96
153
|
permissions: [...new Set<string>((module.requestPermissions || []).map(item => item.name))].sort(),
|
|
97
154
|
querySchemes: [...new Set<string>(module.querySchemes || [])].sort(),
|
|
98
155
|
backgroundModes: [...new Set<string>(entry.backgroundModes || [])].sort(),
|
package/src/runtime/contract.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { createRequire } from 'node:module';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
5
5
|
import { createNativeModuleContracts, ohpmDependenciesFromManifest, verifyModulesAsync, type Manifest, type ModuleDescriptor } from '@expo-harmony/expo-modules-autolinking';
|
|
6
|
-
import { assertHarmonyCompatibility, validateHarmonyRuntime, type HarmonyRuntimeContract } from '@expo-harmony/expo-modules-autolinking/runtime';
|
|
6
|
+
import { assertHarmonyCompatibility, harmonyRuntimeSchemaVersion, validateHarmonyRuntime, type HarmonyRuntimeContract } from '@expo-harmony/expo-modules-autolinking/runtime';
|
|
7
7
|
import JSON5 from 'json5';
|
|
8
8
|
|
|
9
9
|
import { publishUpdatesConfigurationAsync } from '../updates/export';
|
|
@@ -33,7 +33,7 @@ async function createRuntimeContractAsync(
|
|
|
33
33
|
const native = createRequire(require.resolve(resolution.harmonyPackage + '/package.json'));
|
|
34
34
|
|
|
35
35
|
const contract: HarmonyRuntimeContract = {
|
|
36
|
-
schemaVersion:
|
|
36
|
+
schemaVersion: harmonyRuntimeSchemaVersion(project.native),
|
|
37
37
|
platform: 'harmony',
|
|
38
38
|
runtimeVersion: project.runtimeVersion,
|
|
39
39
|
development: true,
|
|
@@ -143,8 +143,10 @@ export async function publishRuntimeContractAsync(
|
|
|
143
143
|
|
|
144
144
|
const native = readNativeRuntime(plan);
|
|
145
145
|
requirements.development = plan.buildMode === 'debug';
|
|
146
|
+
|
|
146
147
|
const contract: HarmonyRuntimeContract = {
|
|
147
148
|
...requirements,
|
|
149
|
+
schemaVersion: harmonyRuntimeSchemaVersion(native.config),
|
|
148
150
|
runtimeVersion: await resolveRuntimeVersionAsync(root, project.config, native.app),
|
|
149
151
|
config: native.config,
|
|
150
152
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
|
+
import { getConfig } from '@expo/config';
|
|
4
5
|
import { createFingerprintAsync, type HashSource } from '@expo/fingerprint';
|
|
5
6
|
import { createNativeModuleContracts, verifyModulesAsync } from '@expo-harmony/expo-modules-autolinking';
|
|
6
7
|
import { HarmonyCliError } from '../errors';
|
|
@@ -10,18 +11,27 @@ export async function fingerprintHarmonyAsync(root: string) {
|
|
|
10
11
|
if (!result.valid) throw new HarmonyCliError('ERR_HARMONY_FINGERPRINT', 'Build and verify Harmony native modules before calculating the runtime fingerprint.', { operation: 'fingerprint' });
|
|
11
12
|
|
|
12
13
|
const require = createRequire(path.join(root, 'package.json'));
|
|
13
|
-
const
|
|
14
|
+
const modules = createNativeModuleContracts(result.modules);
|
|
15
|
+
const { exp } = getConfig(root, { skipSDKVersionRequirement: true, isModdedConfig: true });
|
|
14
16
|
const generated: string[] = [];
|
|
15
17
|
const previous = path.join(root, '.expo/harmony/export-manifest.json');
|
|
18
|
+
|
|
16
19
|
if (fs.existsSync(previous)) {
|
|
17
20
|
const manifest = JSON.parse(fs.readFileSync(previous, 'utf8'));
|
|
18
21
|
generated.push(...manifest.assets.map(asset => `**/harmony/**/src/main/resources/rawfile/${asset.path}`));
|
|
19
22
|
}
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
|
|
24
|
+
const sources: HashSource[] = [{ type: 'contents', id: 'harmonyAutolinking', contents: JSON.stringify(modules), reasons: ['harmonyAutolinking'] }];
|
|
25
|
+
|
|
26
|
+
for (const name of ['@react-native-oh/react-native-harmony', '@expo-harmony/template', '@expo-harmony/config-plugins', '@expo-harmony/prebuild-config', '@expo-harmony/expo-build-properties']) {
|
|
22
27
|
const file = require.resolve(name + '/package.json');
|
|
23
28
|
sources.push({ type: 'dir', filePath: path.relative(root, path.dirname(file)), reasons: ['harmonyNativeToolchain'] });
|
|
24
29
|
}
|
|
30
|
+
|
|
31
|
+
for (const file of exp._internal?.harmonyBuildProperties?.release?.obfuscation?.files ?? []) {
|
|
32
|
+
sources.push({ type: 'file', filePath: file, reasons: ['harmonyObfuscationRules'] });
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
if (fs.existsSync(path.join(root, 'harmony'))) sources.push({ type: 'dir', filePath: 'harmony', reasons: ['harmonyNativeProject'] });
|
|
26
36
|
|
|
27
37
|
return createFingerprintAsync(root, {
|
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 };
|