@expo-harmony/cli 55.0.26-harmony.10 → 55.0.26-harmony.11
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 +1 -1
- package/build/development/manifest.js +23 -2
- package/build/development/protocol.d.ts +1 -0
- package/build/development/protocol.js +12 -5
- package/build/run/devices.js +7 -0
- 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/package.json +4 -4
- package/src/development/manifest.ts +24 -3
- package/src/development/protocol.ts +11 -6
- package/src/run/devices.ts +8 -0
- package/src/runtime/config.ts +64 -7
- package/src/runtime/contract.ts +4 -2
- package/src/runtime/fingerprint.ts +13 -3
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
|
|
|
@@ -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/run/devices.js
CHANGED
|
@@ -159,6 +159,13 @@ async function installHapAsync(hdc, device, hap, options = {}) {
|
|
|
159
159
|
});
|
|
160
160
|
}
|
|
161
161
|
async function configureMetroPortAsync(hdc, device, port, options = {}) {
|
|
162
|
+
if (options.devicePort === undefined && port !== 8081) {
|
|
163
|
+
// RNOH's default provider uses 8081; launcher manifests retain Metro's
|
|
164
|
+
// advertised port for the bundle, assets, HMR and inspector URLs.
|
|
165
|
+
await configureMetroPortAsync(hdc, device, port, { ...options, devicePort: 8081 });
|
|
166
|
+
await configureMetroPortAsync(hdc, device, port, { ...options, devicePort: port });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
162
169
|
const deviceEndpoint = `tcp:${options.devicePort || 8081}`;
|
|
163
170
|
const hostEndpoint = `tcp:${port}`;
|
|
164
171
|
const forwards = await runHdcAsync(hdc, ['-t', device.id, 'fport', 'ls'], {
|
|
@@ -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, {
|
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.11",
|
|
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/run/devices.ts
CHANGED
|
@@ -233,6 +233,14 @@ async function configureMetroPortAsync(
|
|
|
233
233
|
port: number,
|
|
234
234
|
options: HdcOptions = {}
|
|
235
235
|
): Promise<void> {
|
|
236
|
+
if (options.devicePort === undefined && port !== 8081) {
|
|
237
|
+
// RNOH's default provider uses 8081; launcher manifests retain Metro's
|
|
238
|
+
// advertised port for the bundle, assets, HMR and inspector URLs.
|
|
239
|
+
await configureMetroPortAsync(hdc, device, port, { ...options, devicePort: 8081 });
|
|
240
|
+
await configureMetroPortAsync(hdc, device, port, { ...options, devicePort: port });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
236
244
|
const deviceEndpoint = `tcp:${options.devicePort || 8081}`;
|
|
237
245
|
const hostEndpoint = `tcp:${port}`;
|
|
238
246
|
|
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, {
|