@expo-harmony/cli 55.0.26-harmony.1 → 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 +87 -38
- package/build/args.d.ts +19 -0
- package/build/args.js +27 -0
- package/build/bin/expo-harmony.d.ts +2 -0
- package/build/bin/expo-harmony.js +1 -1
- package/build/buildHap/build.d.ts +18 -0
- package/build/buildHap/build.js +38 -0
- package/build/buildHap/index.d.ts +2 -0
- package/build/buildHap/index.js +19 -0
- package/build/buildHap/options.d.ts +7 -0
- package/build/buildHap/options.js +30 -0
- package/build/cli.d.ts +3 -0
- package/build/cli.js +40 -0
- package/build/command.d.ts +9 -0
- package/build/command.js +20 -0
- package/build/development/manifest.d.ts +1 -0
- package/build/development/manifest.js +121 -0
- package/build/development/protocol.d.ts +3 -0
- package/build/development/protocol.js +33 -0
- package/build/development/session.d.ts +5 -0
- package/build/development/session.js +66 -0
- package/build/doctor/doctor.d.ts +20 -0
- package/build/doctor/doctor.js +226 -0
- package/build/doctor/index.d.ts +2 -0
- package/build/doctor/index.js +16 -0
- package/build/doctor/options.d.ts +5 -0
- package/build/doctor/options.js +15 -0
- package/build/entry.d.ts +2 -0
- package/build/entry.js +47 -0
- package/build/errors.d.ts +12 -0
- package/build/errors.js +16 -0
- package/build/expo.d.ts +19 -0
- package/build/expo.js +53 -0
- package/build/exportEmbed/export.d.ts +18 -0
- package/build/exportEmbed/export.js +124 -0
- package/build/exportEmbed/index.d.ts +2 -0
- package/build/exportEmbed/index.js +19 -0
- package/build/exportEmbed/manifest.d.ts +44 -0
- package/build/exportEmbed/manifest.js +171 -0
- package/build/exportEmbed/options.d.ts +7 -0
- package/build/exportEmbed/options.js +22 -0
- package/build/file.d.ts +10 -0
- package/build/file.js +110 -0
- package/build/help.d.ts +1 -0
- package/build/help.js +44 -0
- package/build/index.d.ts +14 -0
- package/build/index.js +6 -5
- package/build/internal/development.d.ts +1 -0
- package/build/internal/development.js +8 -0
- package/build/internal/export.d.ts +1 -0
- package/build/internal/export.js +80 -0
- package/build/internal/prebuild.d.ts +3 -0
- package/build/internal/prebuild.js +9 -0
- package/build/log.d.ts +4 -0
- package/build/log.js +6 -0
- package/build/modules/index.d.ts +2 -0
- package/build/modules/index.js +16 -0
- package/build/modules/modules.d.ts +42 -0
- package/build/modules/modules.js +84 -0
- package/build/modules/options.d.ts +10 -0
- package/build/modules/options.js +45 -0
- package/build/native/bare.d.ts +4 -0
- package/build/native/bare.js +130 -0
- package/build/native/build.d.ts +15 -0
- package/build/native/build.js +69 -0
- package/build/native/cache.d.ts +11 -0
- package/build/native/cache.js +73 -0
- package/build/native/hvigor.d.ts +3 -0
- package/build/native/hvigor.js +83 -0
- package/build/native/install.d.ts +5 -0
- package/build/native/install.js +13 -0
- package/build/native/prepareProject.d.ts +11 -0
- package/build/native/prepareProject.js +68 -0
- package/build/native/project.d.ts +8 -0
- package/build/native/project.js +58 -0
- package/build/native/toolchain.d.ts +16 -0
- package/build/native/toolchain.js +172 -0
- package/build/native/types.d.ts +47 -0
- package/build/native/types.js +2 -0
- package/build/path.d.ts +4 -0
- package/build/path.js +27 -0
- package/build/prebuild/check.d.ts +10 -0
- package/build/prebuild/check.js +151 -0
- package/build/prebuild/clean.d.ts +2 -0
- package/build/prebuild/clean.js +50 -0
- package/build/prebuild/index.d.ts +2 -0
- package/build/prebuild/index.js +51 -0
- package/build/prebuild/options.d.ts +10 -0
- package/build/prebuild/options.js +50 -0
- package/build/prebuild/prebuild.d.ts +6 -0
- package/build/prebuild/prebuild.js +70 -0
- package/build/prebuild/template.d.ts +8 -0
- package/build/prebuild/template.js +86 -0
- package/build/process.d.ts +41 -0
- package/build/process.js +324 -0
- package/build/profile.d.ts +1 -0
- package/build/profile.js +12 -0
- package/build/project.d.ts +2 -0
- package/build/project.js +23 -0
- package/build/projectLock.d.ts +2 -0
- package/build/projectLock.js +185 -0
- package/build/run/devices.d.ts +29 -0
- package/build/run/devices.js +223 -0
- package/build/run/emulators.d.ts +13 -0
- package/build/run/emulators.js +82 -0
- package/build/run/index.d.ts +2 -0
- package/build/run/index.js +25 -0
- package/build/run/metro.d.ts +21 -0
- package/build/run/metro.js +162 -0
- package/build/run/options.d.ts +14 -0
- package/build/run/options.js +62 -0
- package/build/run/run.d.ts +46 -0
- package/build/run/run.js +135 -0
- package/build/runtime/config.d.ts +37 -0
- package/build/runtime/config.js +169 -0
- package/build/runtime/contract.d.ts +5 -0
- package/build/runtime/contract.js +109 -0
- package/build/runtime/fingerprint.d.ts +1 -0
- package/build/runtime/fingerprint.js +49 -0
- package/build/runtime/index.d.ts +2 -0
- package/build/runtime/index.js +50 -0
- package/build/start/index.d.ts +2 -0
- package/build/start/index.js +38 -0
- package/build/start/options.d.ts +9 -0
- package/build/start/options.js +38 -0
- package/build/updates/export.d.ts +14 -0
- package/build/updates/export.js +46 -0
- package/build/updates/index.d.ts +2 -0
- package/build/updates/index.js +96 -0
- package/build/upstream.d.ts +9 -0
- package/build/upstream.js +17 -0
- package/package.json +21 -5
- package/src/bin/expo-harmony.ts +2 -2
- package/src/buildHap/build.ts +13 -105
- package/src/buildHap/index.ts +19 -0
- package/src/cli.ts +27 -225
- package/src/command.ts +24 -0
- package/src/development/manifest.ts +127 -0
- package/src/development/protocol.ts +42 -0
- package/src/development/session.ts +83 -0
- package/src/doctor/doctor.ts +67 -62
- package/src/doctor/index.ts +16 -0
- package/src/exportEmbed/export.ts +73 -72
- package/src/exportEmbed/index.ts +17 -0
- package/src/file.ts +20 -11
- package/src/help.ts +41 -0
- package/src/index.ts +5 -3
- package/src/internal/development.ts +8 -0
- package/src/internal/export.ts +82 -0
- package/src/internal/prebuild.ts +3 -0
- package/src/log.ts +5 -0
- package/src/modules/index.ts +16 -0
- package/src/native/bare.ts +145 -0
- package/src/native/build.ts +83 -0
- package/src/{run → native}/cache.ts +10 -9
- package/src/native/hvigor.ts +88 -0
- package/src/native/install.ts +17 -0
- package/src/native/prepareProject.ts +99 -0
- package/src/native/project.ts +88 -0
- package/src/{tools.ts → native/toolchain.ts} +38 -140
- package/src/native/types.ts +44 -0
- package/src/prebuild/check.ts +56 -31
- package/src/prebuild/clean.ts +3 -3
- package/src/prebuild/index.ts +56 -0
- package/src/prebuild/prebuild.ts +11 -1
- package/src/prebuild/template.ts +2 -2
- package/src/process.ts +175 -64
- package/src/profile.ts +13 -0
- package/src/projectLock.ts +9 -1
- package/src/run/devices.ts +24 -11
- package/src/run/emulators.ts +4 -6
- package/src/run/index.ts +25 -0
- package/src/run/metro.ts +10 -1
- package/src/run/options.ts +2 -0
- package/src/run/run.ts +63 -129
- package/src/runtime/config.ts +207 -0
- package/src/runtime/contract.ts +167 -0
- package/src/runtime/fingerprint.ts +49 -0
- package/src/runtime/index.ts +54 -0
- package/src/start/index.ts +37 -0
- package/src/start/options.ts +12 -0
- package/src/updates/export.ts +60 -0
- package/src/updates/index.ts +89 -0
- package/src/buildHap/common.ts +0 -145
- package/src/run/install.ts +0 -41
package/src/doctor/doctor.ts
CHANGED
|
@@ -5,17 +5,14 @@ import path from 'node:path';
|
|
|
5
5
|
import { getConfig } from '@expo/config';
|
|
6
6
|
import { normalizeHarmonyConfig } from '@expo-harmony/config-plugins';
|
|
7
7
|
import { verifyModulesAsync } from '@expo-harmony/expo-modules-autolinking';
|
|
8
|
-
import { isRnohAutolinkingDisabled } from '@expo-harmony/prebuild-config/
|
|
9
|
-
import { validateHarmonySigningConfigFile } from '@expo-harmony/prebuild-config/signing';
|
|
8
|
+
import { isRnohAutolinkingDisabled, validateHarmonySigningConfigFile } from '@expo-harmony/prebuild-config/internal';
|
|
10
9
|
|
|
11
10
|
import { spawnAsync } from '../process';
|
|
12
11
|
import { withHarmonyProjectLockAsync } from '../projectLock';
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
resolveHarmonyToolchain,
|
|
16
|
-
type HarmonyTool,
|
|
17
|
-
} from '../tools';
|
|
12
|
+
import { resolveHarmonyBuildPlanIfPresentAsync } from '../native/project';
|
|
13
|
+
import { resolveHarmonyToolchain, type HarmonyTool } from '../native/toolchain';
|
|
18
14
|
import { RequiredProjectPackages } from '../upstream';
|
|
15
|
+
import { isBareHarmonyProject } from '../native/bare';
|
|
19
16
|
|
|
20
17
|
export interface DoctorCheck {
|
|
21
18
|
details?: unknown;
|
|
@@ -37,22 +34,23 @@ interface DoctorOptions {
|
|
|
37
34
|
validateModules?: boolean;
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
function hasPlugin(plugins,
|
|
41
|
-
return (plugins || []).some(plugin => (Array.isArray(plugin) ? plugin[0] : plugin) ===
|
|
37
|
+
function hasPlugin(plugins, name) {
|
|
38
|
+
return (plugins || []).some(plugin => (Array.isArray(plugin) ? plugin[0] : plugin) === name);
|
|
42
39
|
}
|
|
43
40
|
|
|
44
41
|
function check(id: string, status: DoctorCheck['status'], message: string, details?: unknown): DoctorCheck {
|
|
45
42
|
return { id, status, message, ...(details ? { details } : {}) };
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
function canResolvePackage(
|
|
49
|
-
const
|
|
45
|
+
function canResolvePackage(root, name) {
|
|
46
|
+
const require = createRequire(path.join(root, 'package.json'));
|
|
47
|
+
|
|
50
48
|
try {
|
|
51
|
-
|
|
49
|
+
require.resolve(`${name}/package.json`);
|
|
52
50
|
return true;
|
|
53
51
|
} catch {
|
|
54
52
|
try {
|
|
55
|
-
|
|
53
|
+
require.resolve(name);
|
|
56
54
|
return true;
|
|
57
55
|
} catch {
|
|
58
56
|
return false;
|
|
@@ -60,20 +58,20 @@ function canResolvePackage(projectRoot, packageName) {
|
|
|
60
58
|
}
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
async function validateMetroConfigAsync(
|
|
64
|
-
const
|
|
65
|
-
let
|
|
61
|
+
async function validateMetroConfigAsync(root) {
|
|
62
|
+
const require = createRequire(path.join(root, 'package.json'));
|
|
63
|
+
let metro;
|
|
66
64
|
|
|
67
65
|
try {
|
|
68
|
-
|
|
66
|
+
metro = require('metro-config');
|
|
69
67
|
} catch (cause) {
|
|
70
68
|
throw new Error('Cannot load the project-local metro-config package.', { cause });
|
|
71
69
|
}
|
|
72
70
|
|
|
73
|
-
if (typeof
|
|
71
|
+
if (typeof metro.resolveConfig !== 'function') {
|
|
74
72
|
const version = (() => {
|
|
75
73
|
try {
|
|
76
|
-
return
|
|
74
|
+
return require('metro-config/package.json').version;
|
|
77
75
|
} catch {
|
|
78
76
|
return 'unknown';
|
|
79
77
|
}
|
|
@@ -82,15 +80,15 @@ async function validateMetroConfigAsync(projectRoot) {
|
|
|
82
80
|
throw new Error(`metro-config ${version} does not expose resolveConfig().`);
|
|
83
81
|
}
|
|
84
82
|
|
|
85
|
-
const
|
|
86
|
-
const
|
|
83
|
+
const present = Object.hasOwn(process.env, 'EXPO_METRO_TARGET');
|
|
84
|
+
const previous = process.env.EXPO_METRO_TARGET;
|
|
87
85
|
let resolved;
|
|
88
86
|
|
|
89
87
|
try {
|
|
90
88
|
process.env.EXPO_METRO_TARGET = 'harmony';
|
|
91
|
-
resolved = await
|
|
89
|
+
resolved = await metro.resolveConfig(undefined, root);
|
|
92
90
|
} finally {
|
|
93
|
-
if (
|
|
91
|
+
if (present) process.env.EXPO_METRO_TARGET = previous;
|
|
94
92
|
else delete process.env.EXPO_METRO_TARGET;
|
|
95
93
|
}
|
|
96
94
|
|
|
@@ -105,33 +103,40 @@ async function validateMetroConfigAsync(projectRoot) {
|
|
|
105
103
|
}
|
|
106
104
|
}
|
|
107
105
|
|
|
108
|
-
async function doctorUnlockedAsync(
|
|
106
|
+
async function doctorUnlockedAsync(root: string, options: DoctorOptions = {}): Promise<DoctorResult> {
|
|
109
107
|
const checks: DoctorCheck[] = [];
|
|
110
|
-
const
|
|
108
|
+
const unavailable = options.requireBuildTools ? 'error' : 'warn';
|
|
111
109
|
let config;
|
|
110
|
+
const bare = isBareHarmonyProject(root);
|
|
112
111
|
|
|
113
112
|
try {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
113
|
+
if (bare) {
|
|
114
|
+
const plan = await resolveHarmonyBuildPlanIfPresentAsync(root);
|
|
115
|
+
checks.push(check('native-config', 'pass', `Bare Harmony project: ${plan.bundleName} / ${plan.moduleName}. Native files own the configuration.`));
|
|
116
|
+
} else {
|
|
117
|
+
config = getConfig(root, {
|
|
118
|
+
isModdedConfig: true,
|
|
119
|
+
skipSDKVersionRequirement: true,
|
|
120
|
+
}).exp;
|
|
121
|
+
normalizeHarmonyConfig(config);
|
|
122
|
+
checks.push(check('app-config', 'pass', 'Harmony app config is valid.'));
|
|
123
|
+
}
|
|
120
124
|
} catch (error) {
|
|
121
|
-
checks.push(check('app-config', 'error', error.message, { code: error.code || 'ERR_HARMONY_CONFIG_INVALID' }));
|
|
125
|
+
checks.push(check(bare ? 'native-config' : 'app-config', 'error', error.message, { code: error.code || 'ERR_HARMONY_CONFIG_INVALID' }));
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
if (config) {
|
|
125
129
|
checks.push(hasPlugin(config.plugins, '@expo-harmony/prebuild-config')
|
|
126
130
|
? check('config-plugin', 'pass', '@expo-harmony/prebuild-config is registered.')
|
|
127
131
|
: check('config-plugin', 'error', 'Add @expo-harmony/prebuild-config to expo.plugins.'));
|
|
132
|
+
|
|
128
133
|
const harmony = (config as typeof config & {
|
|
129
134
|
harmony?: { signingConfigFile?: string };
|
|
130
135
|
}).harmony;
|
|
131
136
|
|
|
132
137
|
if (harmony?.signingConfigFile) {
|
|
133
138
|
try {
|
|
134
|
-
const signing = await validateHarmonySigningConfigFile(
|
|
139
|
+
const signing = await validateHarmonySigningConfigFile(root, harmony.signingConfigFile);
|
|
135
140
|
checks.push(check('signing', 'pass', `Harmony signing config ${signing.name} is valid.`));
|
|
136
141
|
} catch (error) {
|
|
137
142
|
checks.push(check('signing', 'error', error.message, { code: error.code || 'ERR_HARMONY_SIGNING_INVALID' }));
|
|
@@ -142,23 +147,23 @@ async function doctorUnlockedAsync(projectRoot: string, options: DoctorOptions =
|
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
try {
|
|
145
|
-
await validateMetroConfigAsync(
|
|
150
|
+
await validateMetroConfigAsync(root);
|
|
146
151
|
checks.push(check('metro', 'pass', 'The resolved Metro config enables Harmony.'));
|
|
147
152
|
} catch (error) {
|
|
148
153
|
checks.push(check('metro', 'error', `Cannot load a Harmony-enabled Metro config: ${error.message}`, { code: error.code || 'ERR_HARMONY_METRO_CONFIG' }));
|
|
149
154
|
}
|
|
150
155
|
|
|
151
|
-
for (const
|
|
152
|
-
if (canResolvePackage(
|
|
153
|
-
checks.push(check(`package:${
|
|
156
|
+
for (const name of RequiredProjectPackages) {
|
|
157
|
+
if (canResolvePackage(root, name)) {
|
|
158
|
+
checks.push(check(`package:${name}`, 'pass', `${name} is resolvable.`));
|
|
154
159
|
} else {
|
|
155
|
-
checks.push(check(`package:${
|
|
160
|
+
checks.push(check(`package:${name}`, 'error', `${name} is not resolvable from the app.`));
|
|
156
161
|
}
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
if (options.validateModules !== false) {
|
|
160
165
|
try {
|
|
161
|
-
const result = await verifyModulesAsync({ platform: 'harmony', projectRoot });
|
|
166
|
+
const result = await verifyModulesAsync({ platform: 'harmony', projectRoot: root });
|
|
162
167
|
const errors = result.diagnostics.filter(item => item.severity === 'error');
|
|
163
168
|
const warnings = result.diagnostics.filter(item => item.severity === 'warning');
|
|
164
169
|
|
|
@@ -190,51 +195,55 @@ async function doctorUnlockedAsync(projectRoot: string, options: DoctorOptions =
|
|
|
190
195
|
}
|
|
191
196
|
|
|
192
197
|
const toolchain = resolveHarmonyToolchain();
|
|
193
|
-
let
|
|
198
|
+
let sdk;
|
|
194
199
|
|
|
195
200
|
if (toolchain.sdkHome) {
|
|
196
|
-
|
|
201
|
+
sdk = check('harmony-sdk', 'pass', `Complete Harmony SDK root was resolved at ${toolchain.sdkHome}.`);
|
|
197
202
|
} else {
|
|
198
|
-
|
|
203
|
+
sdk = check('harmony-sdk', unavailable, 'No complete Harmony SDK root with HMS and OpenHarmony components was found; generation works but HAP build cannot be verified.');
|
|
199
204
|
}
|
|
200
205
|
|
|
201
|
-
checks.push(
|
|
206
|
+
checks.push(sdk);
|
|
202
207
|
|
|
203
208
|
const tools: Array<[string, HarmonyTool, string[], DoctorCheck['status']]> = [
|
|
204
|
-
['ohpm', toolchain.ohpm, ['--version'],
|
|
205
|
-
['hvigor-command', toolchain.hvigor, ['--version'],
|
|
209
|
+
['ohpm', toolchain.ohpm, ['--version'], unavailable],
|
|
210
|
+
['hvigor-command', toolchain.hvigor, ['--version'], unavailable],
|
|
206
211
|
];
|
|
207
212
|
if (options.requireDeviceTools !== false) {
|
|
208
|
-
tools.unshift(['hdc', toolchain.hdc, ['-v'],
|
|
213
|
+
tools.unshift(['hdc', toolchain.hdc, ['-v'], unavailable]);
|
|
209
214
|
}
|
|
210
|
-
|
|
215
|
+
|
|
216
|
+
for (const [id, tool, args, status] of tools) {
|
|
217
|
+
const command = [tool.command, ...tool.args].map(value => JSON.stringify(value)).join(' ');
|
|
218
|
+
|
|
211
219
|
try {
|
|
212
|
-
const result = await spawnAsync(tool.command, [...tool.args, ...
|
|
220
|
+
const result = await spawnAsync(tool.command, [...tool.args, ...args], {
|
|
213
221
|
capture: true,
|
|
214
|
-
cwd:
|
|
222
|
+
cwd: root,
|
|
215
223
|
operation: `doctor-${id}`,
|
|
216
224
|
timeoutMs: 10_000,
|
|
217
225
|
});
|
|
226
|
+
|
|
218
227
|
checks.push(result.code === 0 && !result.timedOut
|
|
219
|
-
? check(id, 'pass', `${
|
|
220
|
-
: check(id,
|
|
228
|
+
? check(id, 'pass', `${command} is available through ${tool.source}.`)
|
|
229
|
+
: check(id, status, `${command} is unavailable or unhealthy; HAP build cannot be verified.`));
|
|
221
230
|
} catch {
|
|
222
|
-
checks.push(check(id,
|
|
231
|
+
checks.push(check(id, status, `${command} is unavailable; generation remains available.`));
|
|
223
232
|
}
|
|
224
233
|
}
|
|
225
234
|
|
|
226
235
|
if (options.validateGeneratedProject !== false) {
|
|
227
236
|
try {
|
|
228
|
-
const plan = await resolveHarmonyBuildPlanIfPresentAsync(
|
|
237
|
+
const plan = await resolveHarmonyBuildPlanIfPresentAsync(root);
|
|
229
238
|
if (plan && fs.existsSync(plan.harmonyRoot)) {
|
|
230
239
|
if (!fs.existsSync(plan.projectFiles.rootHvigor)
|
|
231
240
|
|| !fs.existsSync(plan.projectFiles.moduleHvigor)) {
|
|
232
|
-
checks.push(check('hvigor', 'error', '
|
|
241
|
+
checks.push(check('hvigor', 'error', 'Root and module Hvigor files are required.'));
|
|
233
242
|
} else {
|
|
234
243
|
const content = await fs.promises.readFile(plan.projectFiles.moduleHvigor, 'utf8');
|
|
235
244
|
checks.push(isRnohAutolinkingDisabled(content)
|
|
236
245
|
? check('hvigor-autolinking', 'pass', 'RNOH duplicate autolinking is disabled.')
|
|
237
|
-
: check('hvigor-autolinking', 'error', 'The
|
|
246
|
+
: check('hvigor-autolinking', 'error', 'The module Hvigor file must disable duplicate RNOH autolinking.'));
|
|
238
247
|
}
|
|
239
248
|
}
|
|
240
249
|
} catch (error) {
|
|
@@ -245,16 +254,12 @@ async function doctorUnlockedAsync(projectRoot: string, options: DoctorOptions =
|
|
|
245
254
|
return {
|
|
246
255
|
checks,
|
|
247
256
|
ok: checks.every(item => item.status !== 'error'),
|
|
248
|
-
projectRoot,
|
|
257
|
+
projectRoot: root,
|
|
249
258
|
};
|
|
250
259
|
}
|
|
251
260
|
|
|
252
|
-
async function doctorAsync(
|
|
253
|
-
return withHarmonyProjectLockAsync(
|
|
254
|
-
projectRoot,
|
|
255
|
-
'doctor',
|
|
256
|
-
() => doctorUnlockedAsync(projectRoot, options)
|
|
257
|
-
);
|
|
261
|
+
async function doctorAsync(root: string, options: DoctorOptions = {}): Promise<DoctorResult> {
|
|
262
|
+
return withHarmonyProjectLockAsync(root, 'doctor', () => doctorUnlockedAsync(root, options));
|
|
258
263
|
}
|
|
259
264
|
|
|
260
265
|
function formatDoctor(result: DoctorResult): string {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { parseCommandArgs, type Command } from '../command';
|
|
2
|
+
import { parseDoctorArgs } from './options';
|
|
3
|
+
|
|
4
|
+
export const command: Command = async (argv, io) => {
|
|
5
|
+
const args = parseCommandArgs(parseDoctorArgs, argv, io);
|
|
6
|
+
if (!args) return 0;
|
|
7
|
+
|
|
8
|
+
const { projectRoot: root } = args;
|
|
9
|
+
const { doctorAsync, formatDoctor } = await import('./doctor.js');
|
|
10
|
+
|
|
11
|
+
const result = await doctorAsync(root, { requireBuildTools: true });
|
|
12
|
+
|
|
13
|
+
io.log(formatDoctor(result));
|
|
14
|
+
|
|
15
|
+
return result.ok ? 0 : 1;
|
|
16
|
+
};
|
|
@@ -3,13 +3,14 @@ import { createRequire } from 'node:module';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { publishEmbeddedUpdateAsync } from '../updates/export';
|
|
7
|
+
import { doctorAsync } from '../doctor/doctor';
|
|
7
8
|
import { resolveHarmonyEntryPoint } from '../entry';
|
|
8
9
|
import { HarmonyCliError } from '../errors';
|
|
9
10
|
import { withHarmonyProjectLockAsync } from '../projectLock';
|
|
10
|
-
import { resolveHarmonyBuildPlanAsync } from '../
|
|
11
|
+
import { resolveHarmonyBuildPlanAsync } from '../native/project';
|
|
11
12
|
import { formatDiagnostics, spawnAsync } from '../process';
|
|
12
|
-
import {
|
|
13
|
+
import { resolveExpoHermesBuilder } from '../expo';
|
|
13
14
|
import {
|
|
14
15
|
assertHermesBundle, assertSourceMap, exportPaths,
|
|
15
16
|
publishExportAsync, validatePublishedExportAsync,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
|
|
19
20
|
export interface ExportOptions {
|
|
20
21
|
check?: boolean;
|
|
22
|
+
outputDirectory?: string;
|
|
21
23
|
resetCache?: boolean;
|
|
22
24
|
skipDoctor?: boolean;
|
|
23
25
|
timeoutMs?: number;
|
|
@@ -28,69 +30,56 @@ export interface ExportTemporary {
|
|
|
28
30
|
bundle: string;
|
|
29
31
|
javascript: string;
|
|
30
32
|
metroSourceMap: string;
|
|
33
|
+
metadata: string;
|
|
31
34
|
sourceMap: string;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
function createExpoExportEmbedArgs(
|
|
35
|
-
projectRoot: string,
|
|
36
|
-
entryFile: string,
|
|
37
|
-
temporary: ExportTemporary,
|
|
38
|
-
options: ExportOptions = {}
|
|
39
|
-
) {
|
|
40
|
-
return [
|
|
41
|
-
'export:embed',
|
|
42
|
-
'--platform', 'harmony',
|
|
43
|
-
'--entry-file', entryFile,
|
|
44
|
-
'--bundle-output', temporary.javascript,
|
|
45
|
-
'--assets-dest', temporary.assets,
|
|
46
|
-
'--dev', 'false',
|
|
47
|
-
'--minify', 'false',
|
|
48
|
-
'--sourcemap-output', temporary.metroSourceMap,
|
|
49
|
-
'--sourcemap-sources-root', '.',
|
|
50
|
-
'--unstable-transform-profile', 'hermes-stable',
|
|
51
|
-
...(options.resetCache ? ['--reset-cache=true'] : []),
|
|
52
|
-
projectRoot,
|
|
53
|
-
];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function assertDoctor(result: DoctorResult) {
|
|
57
|
-
if (result.ok) return;
|
|
58
|
-
|
|
59
|
-
const failing = result.checks.filter(check => check.status === 'error').map(check => check.id);
|
|
60
|
-
throw new HarmonyCliError('ERR_HARMONY_EXPORT_DOCTOR', `Harmony doctor found blocking checks: ${failing.join(', ') || 'unknown'}.`, { operation: 'doctor' });
|
|
61
|
-
}
|
|
62
|
-
|
|
63
37
|
async function exportEmbedUnlockedAsync(
|
|
64
|
-
|
|
38
|
+
root: string,
|
|
65
39
|
options: ExportOptions = {}
|
|
66
40
|
): Promise<HarmonyExportManifest> {
|
|
67
|
-
if (!options.skipDoctor)
|
|
41
|
+
if (!options.skipDoctor) {
|
|
42
|
+
const doctor = await doctorAsync(root);
|
|
43
|
+
if (!doctor.ok) {
|
|
44
|
+
const checks = doctor.checks.filter(check => check.status === 'error').map(check => check.id);
|
|
45
|
+
|
|
46
|
+
throw new HarmonyCliError('ERR_HARMONY_EXPORT_DOCTOR', `Harmony doctor found blocking checks: ${checks.join(', ') || 'unknown'}.`, { operation: 'doctor' });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
68
49
|
|
|
69
|
-
const plan = await resolveHarmonyBuildPlanAsync(
|
|
70
|
-
const paths =
|
|
50
|
+
const plan = await resolveHarmonyBuildPlanAsync(root, { buildMode: 'release' });
|
|
51
|
+
const paths = options.outputDirectory
|
|
52
|
+
? {
|
|
53
|
+
rawfileRoot: path.join(options.outputDirectory, 'embedded'), bundle: path.join(options.outputDirectory, 'embedded/hermes_bundle.hbc'),
|
|
54
|
+
metadataRoot: options.outputDirectory, manifest: path.join(options.outputDirectory, 'export-manifest.json'),
|
|
55
|
+
sourceMap: path.join(options.outputDirectory, 'hermes_bundle.hbc.map'),
|
|
56
|
+
}
|
|
57
|
+
: exportPaths(plan);
|
|
71
58
|
|
|
72
59
|
if (options.check) return await validatePublishedExportAsync(paths);
|
|
73
60
|
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
assets: path.join(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
61
|
+
const entry = resolveHarmonyEntryPoint(root);
|
|
62
|
+
const directory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'expo-harmony-export-'));
|
|
63
|
+
const temp: ExportTemporary = {
|
|
64
|
+
assets: path.join(directory, 'assets'),
|
|
65
|
+
metadata: path.join(directory, 'assets.json'),
|
|
66
|
+
bundle: path.join(directory, 'hermes_bundle.hbc'),
|
|
67
|
+
javascript: path.join(directory, 'index.js'),
|
|
68
|
+
metroSourceMap: path.join(directory, 'index.js.map'),
|
|
69
|
+
sourceMap: path.join(directory, 'hermes_bundle.hbc.map'),
|
|
82
70
|
};
|
|
83
71
|
|
|
84
72
|
try {
|
|
85
|
-
await fs.promises.mkdir(
|
|
73
|
+
await fs.promises.mkdir(temp.assets, { recursive: true });
|
|
86
74
|
|
|
87
|
-
const expo = resolveExpoCli(projectRoot);
|
|
88
75
|
const result = await spawnAsync(process.execPath, [
|
|
89
|
-
|
|
90
|
-
|
|
76
|
+
path.resolve(__dirname, '../internal/export.js'), root,
|
|
77
|
+
JSON.stringify({ update: !!options.outputDirectory, platform: 'harmony', entryFile: entry, bundleOutput: temp.javascript, assetsDest: temp.assets,
|
|
78
|
+
dev: false, minify: false, sourcemapOutput: temp.metroSourceMap, sourcemapSourcesRoot: '.',
|
|
79
|
+
unstableTransformProfile: 'hermes-stable', resetCache: options.resetCache }), temp.metadata,
|
|
91
80
|
], {
|
|
92
81
|
capture: true,
|
|
93
|
-
cwd:
|
|
82
|
+
cwd: root,
|
|
94
83
|
env: {
|
|
95
84
|
...process.env,
|
|
96
85
|
EXPO_METRO_TARGET: 'harmony',
|
|
@@ -104,6 +93,7 @@ async function exportEmbedUnlockedAsync(
|
|
|
104
93
|
|
|
105
94
|
if (result.code !== 0 || result.timedOut) {
|
|
106
95
|
const diagnostics = formatDiagnostics(result);
|
|
96
|
+
|
|
107
97
|
throw new HarmonyCliError(
|
|
108
98
|
'ERR_HARMONY_EXPORT_FAILED',
|
|
109
99
|
`Expo export:embed exited with code ${result.code}${result.timedOut ? ' after timing out' : ''}.${diagnostics ? `\n${diagnostics}` : ''}`,
|
|
@@ -115,24 +105,27 @@ async function exportEmbedUnlockedAsync(
|
|
|
115
105
|
// then explicitly hand its JS and source map to Expo's own Hermes exporter.
|
|
116
106
|
try {
|
|
117
107
|
const [code, map] = await Promise.all([
|
|
118
|
-
fs.promises.readFile(
|
|
119
|
-
fs.promises.readFile(
|
|
108
|
+
fs.promises.readFile(temp.javascript, 'utf8'),
|
|
109
|
+
fs.promises.readFile(temp.metroSourceMap, 'utf8'),
|
|
120
110
|
]);
|
|
121
|
-
const buildHermesBundleAsync = resolveExpoHermesBuilder(
|
|
111
|
+
const buildHermesBundleAsync = resolveExpoHermesBuilder(root);
|
|
112
|
+
|
|
122
113
|
// Expo resolves hermes-compiler from react-native. For Harmony, resolve it
|
|
123
114
|
// from RNOH instead of the app's Android/iOS React Native installation.
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
await fs.promises.
|
|
115
|
+
const require = createRequire(path.join(root, 'package.json'));
|
|
116
|
+
const runtime = path.dirname(require.resolve('@react-native-oh/react-native-harmony/package.json'));
|
|
117
|
+
const modules = path.join(directory, 'node_modules');
|
|
118
|
+
|
|
119
|
+
await fs.promises.mkdir(modules);
|
|
120
|
+
await fs.promises.symlink(runtime, path.join(modules, 'react-native'),
|
|
129
121
|
process.platform === 'win32' ? 'junction' : 'dir');
|
|
122
|
+
|
|
130
123
|
const output = await buildHermesBundleAsync({
|
|
131
124
|
code,
|
|
132
|
-
filename:
|
|
125
|
+
filename: entry,
|
|
133
126
|
map,
|
|
134
127
|
minify: true,
|
|
135
|
-
projectRoot:
|
|
128
|
+
projectRoot: directory,
|
|
136
129
|
});
|
|
137
130
|
|
|
138
131
|
if (!(output?.hbc instanceof Uint8Array) || typeof output.sourcemap !== 'string') {
|
|
@@ -140,11 +133,18 @@ async function exportEmbedUnlockedAsync(
|
|
|
140
133
|
}
|
|
141
134
|
|
|
142
135
|
await Promise.all([
|
|
143
|
-
fs.promises.writeFile(
|
|
144
|
-
fs.promises.writeFile(
|
|
136
|
+
fs.promises.writeFile(temp.bundle, output.hbc),
|
|
137
|
+
fs.promises.writeFile(temp.sourceMap, output.sourcemap),
|
|
145
138
|
]);
|
|
146
139
|
} catch (cause) {
|
|
147
|
-
if (cause instanceof HarmonyCliError)
|
|
140
|
+
if (cause instanceof HarmonyCliError) {
|
|
141
|
+
throw new HarmonyCliError(cause.code, cause.message, {
|
|
142
|
+
cause,
|
|
143
|
+
exitCode: cause.exitCode,
|
|
144
|
+
operation: cause.operation,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
148
|
throw new HarmonyCliError(
|
|
149
149
|
'ERR_HARMONY_EXPORT_HERMES',
|
|
150
150
|
'Expo failed to compile the Harmony bundle to Hermes bytecode.',
|
|
@@ -152,26 +152,27 @@ async function exportEmbedUnlockedAsync(
|
|
|
152
152
|
);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
const bytecode = await assertHermesBundle(
|
|
156
|
-
await assertSourceMap(
|
|
155
|
+
const bytecode = await assertHermesBundle(temp.bundle);
|
|
156
|
+
await assertSourceMap(temp.sourceMap);
|
|
157
|
+
|
|
158
|
+
const manifest = await publishExportAsync(root, paths, temp, entry, bytecode);
|
|
159
|
+
await publishEmbeddedUpdateAsync(root, paths, JSON.parse(await fs.promises.readFile(temp.metadata, 'utf8')));
|
|
157
160
|
|
|
158
|
-
return
|
|
161
|
+
return manifest;
|
|
159
162
|
} finally {
|
|
160
|
-
await fs.promises.rm(
|
|
163
|
+
await fs.promises.rm(directory, { force: true, recursive: true });
|
|
161
164
|
}
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
async function exportEmbedAsync(
|
|
165
|
-
|
|
168
|
+
root: string,
|
|
166
169
|
options: ExportOptions = {}
|
|
167
170
|
): Promise<HarmonyExportManifest> {
|
|
168
171
|
return withHarmonyProjectLockAsync(
|
|
169
|
-
|
|
172
|
+
root,
|
|
170
173
|
'export:embed',
|
|
171
|
-
() => exportEmbedUnlockedAsync(
|
|
174
|
+
() => exportEmbedUnlockedAsync(root, options)
|
|
172
175
|
);
|
|
173
176
|
}
|
|
174
177
|
|
|
175
|
-
export {
|
|
176
|
-
exportEmbedAsync,
|
|
177
|
-
};
|
|
178
|
+
export { exportEmbedAsync };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { parseCommandArgs, type Command } from '../command';
|
|
2
|
+
import { parseExportEmbedArgs } from './options';
|
|
3
|
+
|
|
4
|
+
export const command: Command = async (argv, io) => {
|
|
5
|
+
const args = parseCommandArgs(parseExportEmbedArgs, argv, io);
|
|
6
|
+
if (!args) return 0;
|
|
7
|
+
|
|
8
|
+
const { projectRoot: root, options } = args;
|
|
9
|
+
const { exportEmbedAsync } = await import('./export.js');
|
|
10
|
+
|
|
11
|
+
const result = await exportEmbedAsync(root, options);
|
|
12
|
+
|
|
13
|
+
if (options.check) io.log('Harmony export bundle, assets, and source map are valid.');
|
|
14
|
+
else io.log(`Exported Hermes bytecode ${result.bundle.path} with ${result.assets.length} asset file(s).`);
|
|
15
|
+
|
|
16
|
+
return 0;
|
|
17
|
+
};
|
package/src/file.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@ import { isInside } from './path';
|
|
|
6
6
|
|
|
7
7
|
async function sha256File(file: string): Promise<string> {
|
|
8
8
|
return await new Promise<string>((resolve, reject) => {
|
|
9
|
-
const hash =
|
|
9
|
+
const hash = crypto.createHash('sha256');
|
|
10
10
|
const input = fs.createReadStream(file);
|
|
11
11
|
|
|
12
12
|
input.on('error', reject);
|
|
@@ -70,26 +70,26 @@ async function ensureSafeParent(root: string, destination: string): Promise<void
|
|
|
70
70
|
async function atomicCopy(source: string, destination: string, root: string): Promise<void> {
|
|
71
71
|
await ensureSafeParent(root, destination);
|
|
72
72
|
|
|
73
|
-
const
|
|
73
|
+
const temp = path.join(path.dirname(destination), `.${path.basename(destination)}.expo-${crypto.randomUUID()}.tmp`);
|
|
74
74
|
|
|
75
75
|
try {
|
|
76
|
-
await fs.promises.copyFile(source,
|
|
77
|
-
await fs.promises.rename(
|
|
76
|
+
await fs.promises.copyFile(source, temp, fs.constants.COPYFILE_EXCL);
|
|
77
|
+
await fs.promises.rename(temp, destination);
|
|
78
78
|
} finally {
|
|
79
|
-
await fs.promises.rm(
|
|
79
|
+
await fs.promises.rm(temp, { force: true });
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
async function atomicWriteJson(value: unknown, destination: string, root: string): Promise<void> {
|
|
84
84
|
await ensureSafeParent(root, destination);
|
|
85
85
|
|
|
86
|
-
const
|
|
86
|
+
const temp = path.join(path.dirname(destination), `.${path.basename(destination)}.expo-${crypto.randomUUID()}.tmp`);
|
|
87
87
|
|
|
88
88
|
try {
|
|
89
|
-
await fs.promises.writeFile(
|
|
90
|
-
await fs.promises.rename(
|
|
89
|
+
await fs.promises.writeFile(temp, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx' });
|
|
90
|
+
await fs.promises.rename(temp, destination);
|
|
91
91
|
} finally {
|
|
92
|
-
await fs.promises.rm(
|
|
92
|
+
await fs.promises.rm(temp, { force: true });
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -107,4 +107,13 @@ async function removeEmptyParents(file: string, stop: string): Promise<void> {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
function isNonEmptyRegularFile(file: string): boolean {
|
|
111
|
+
try {
|
|
112
|
+
const stat = fs.lstatSync(file);
|
|
113
|
+
return !stat.isSymbolicLink() && stat.isFile() && stat.size > 0;
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { atomicCopy, atomicWriteJson, describeFile, isNonEmptyRegularFile, listFiles, removeEmptyParents };
|
package/src/help.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const Help = `Usage: expo-harmony <command> [project] [options]
|
|
2
|
+
|
|
3
|
+
Commands:
|
|
4
|
+
start Start Expo Metro for Harmony development
|
|
5
|
+
runtime Describe project requirements; --client file checks compatibility
|
|
6
|
+
runtime --publish Embed capabilities from prepared native build inputs
|
|
7
|
+
prebuild Generate the Harmony native project with Expo CNG
|
|
8
|
+
prebuild --clean Safely recreate the managed Harmony directory
|
|
9
|
+
prebuild --check Compare generated desired state without project writes
|
|
10
|
+
build Build a HAP without selecting or contacting a device
|
|
11
|
+
doctor Validate config, versions, Metro, RNOH, SDK, and signing
|
|
12
|
+
modules list List native module candidates and their discovery source
|
|
13
|
+
modules inspect Resolve Harmony metadata (--package narrows the result)
|
|
14
|
+
modules verify Validate module config, registration conflicts and safe paths
|
|
15
|
+
export Export an OTA update, assets, metadata, and source map
|
|
16
|
+
export:embed Export validated Hermes bytecode, assets, and a source map
|
|
17
|
+
run Build, install, and launch the Harmony app
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--no-install Skip dependency install (prebuild) or HAP install (run)
|
|
21
|
+
--npm|--yarn|--pnpm|--bun
|
|
22
|
+
Select the package manager used by prebuild dependency install
|
|
23
|
+
--skip-dependency-update <packages>
|
|
24
|
+
Preserve comma-separated dependency versions
|
|
25
|
+
--device <id-or-name> Select an HDC target or start a local emulator by name
|
|
26
|
+
--variant <mode> Build debug or release (default: debug)
|
|
27
|
+
--no-bundler Use an already-running Expo Metro server
|
|
28
|
+
--app-id <bundleName>
|
|
29
|
+
Launch another installed app (requires --no-install when different)
|
|
30
|
+
--host <hostname> Advertised HTTP LAN hostname (start)
|
|
31
|
+
--private-key-path <path>
|
|
32
|
+
Sign development manifests using the configured Updates certificate
|
|
33
|
+
--port <number> Metro and device reverse port (default: 8081)
|
|
34
|
+
--sync Re-run prebuild before building
|
|
35
|
+
--output-dir <path> OTA export directory (default: dist)
|
|
36
|
+
--asset-url <url> HTTP(S) root hosting exported OTA assets
|
|
37
|
+
--check Validate an existing export without writing
|
|
38
|
+
--reset-cache Reset Metro while exporting or starting
|
|
39
|
+
-c, --clear Alias for --reset-cache (start)
|
|
40
|
+
-h, --help Show this help
|
|
41
|
+
`;
|