@expo-harmony/cli 55.0.26-harmony.1 → 55.0.26-harmony.10

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.
Files changed (185) hide show
  1. package/README.md +87 -38
  2. package/build/args.d.ts +19 -0
  3. package/build/args.js +27 -0
  4. package/build/bin/expo-harmony.d.ts +2 -0
  5. package/build/bin/expo-harmony.js +1 -1
  6. package/build/buildHap/build.d.ts +18 -0
  7. package/build/buildHap/build.js +38 -0
  8. package/build/buildHap/index.d.ts +2 -0
  9. package/build/buildHap/index.js +19 -0
  10. package/build/buildHap/options.d.ts +7 -0
  11. package/build/buildHap/options.js +30 -0
  12. package/build/cli.d.ts +3 -0
  13. package/build/cli.js +40 -0
  14. package/build/command.d.ts +9 -0
  15. package/build/command.js +20 -0
  16. package/build/development/manifest.d.ts +1 -0
  17. package/build/development/manifest.js +100 -0
  18. package/build/development/protocol.d.ts +2 -0
  19. package/build/development/protocol.js +26 -0
  20. package/build/development/session.d.ts +5 -0
  21. package/build/development/session.js +66 -0
  22. package/build/doctor/doctor.d.ts +20 -0
  23. package/build/doctor/doctor.js +226 -0
  24. package/build/doctor/index.d.ts +2 -0
  25. package/build/doctor/index.js +16 -0
  26. package/build/doctor/options.d.ts +5 -0
  27. package/build/doctor/options.js +15 -0
  28. package/build/entry.d.ts +2 -0
  29. package/build/entry.js +47 -0
  30. package/build/errors.d.ts +12 -0
  31. package/build/errors.js +16 -0
  32. package/build/expo.d.ts +19 -0
  33. package/build/expo.js +53 -0
  34. package/build/exportEmbed/export.d.ts +18 -0
  35. package/build/exportEmbed/export.js +124 -0
  36. package/build/exportEmbed/index.d.ts +2 -0
  37. package/build/exportEmbed/index.js +19 -0
  38. package/build/exportEmbed/manifest.d.ts +44 -0
  39. package/build/exportEmbed/manifest.js +171 -0
  40. package/build/exportEmbed/options.d.ts +7 -0
  41. package/build/exportEmbed/options.js +22 -0
  42. package/build/file.d.ts +10 -0
  43. package/build/file.js +110 -0
  44. package/build/help.d.ts +1 -0
  45. package/build/help.js +44 -0
  46. package/build/index.d.ts +14 -0
  47. package/build/index.js +6 -5
  48. package/build/internal/development.d.ts +1 -0
  49. package/build/internal/development.js +8 -0
  50. package/build/internal/export.d.ts +1 -0
  51. package/build/internal/export.js +80 -0
  52. package/build/internal/prebuild.d.ts +3 -0
  53. package/build/internal/prebuild.js +9 -0
  54. package/build/log.d.ts +4 -0
  55. package/build/log.js +6 -0
  56. package/build/modules/index.d.ts +2 -0
  57. package/build/modules/index.js +16 -0
  58. package/build/modules/modules.d.ts +42 -0
  59. package/build/modules/modules.js +84 -0
  60. package/build/modules/options.d.ts +10 -0
  61. package/build/modules/options.js +45 -0
  62. package/build/native/bare.d.ts +4 -0
  63. package/build/native/bare.js +130 -0
  64. package/build/native/build.d.ts +15 -0
  65. package/build/native/build.js +69 -0
  66. package/build/native/cache.d.ts +11 -0
  67. package/build/native/cache.js +73 -0
  68. package/build/native/hvigor.d.ts +3 -0
  69. package/build/native/hvigor.js +83 -0
  70. package/build/native/install.d.ts +5 -0
  71. package/build/native/install.js +13 -0
  72. package/build/native/prepareProject.d.ts +11 -0
  73. package/build/native/prepareProject.js +68 -0
  74. package/build/native/project.d.ts +8 -0
  75. package/build/native/project.js +58 -0
  76. package/build/native/toolchain.d.ts +16 -0
  77. package/build/native/toolchain.js +172 -0
  78. package/build/native/types.d.ts +47 -0
  79. package/build/native/types.js +2 -0
  80. package/build/path.d.ts +4 -0
  81. package/build/path.js +27 -0
  82. package/build/prebuild/check.d.ts +10 -0
  83. package/build/prebuild/check.js +151 -0
  84. package/build/prebuild/clean.d.ts +2 -0
  85. package/build/prebuild/clean.js +50 -0
  86. package/build/prebuild/index.d.ts +2 -0
  87. package/build/prebuild/index.js +51 -0
  88. package/build/prebuild/options.d.ts +10 -0
  89. package/build/prebuild/options.js +50 -0
  90. package/build/prebuild/prebuild.d.ts +6 -0
  91. package/build/prebuild/prebuild.js +70 -0
  92. package/build/prebuild/template.d.ts +8 -0
  93. package/build/prebuild/template.js +86 -0
  94. package/build/process.d.ts +41 -0
  95. package/build/process.js +324 -0
  96. package/build/profile.d.ts +1 -0
  97. package/build/profile.js +12 -0
  98. package/build/project.d.ts +2 -0
  99. package/build/project.js +23 -0
  100. package/build/projectLock.d.ts +2 -0
  101. package/build/projectLock.js +185 -0
  102. package/build/run/devices.d.ts +29 -0
  103. package/build/run/devices.js +216 -0
  104. package/build/run/emulators.d.ts +13 -0
  105. package/build/run/emulators.js +82 -0
  106. package/build/run/index.d.ts +2 -0
  107. package/build/run/index.js +25 -0
  108. package/build/run/metro.d.ts +21 -0
  109. package/build/run/metro.js +162 -0
  110. package/build/run/options.d.ts +14 -0
  111. package/build/run/options.js +62 -0
  112. package/build/run/run.d.ts +46 -0
  113. package/build/run/run.js +135 -0
  114. package/build/runtime/config.d.ts +35 -0
  115. package/build/runtime/config.js +118 -0
  116. package/build/runtime/contract.d.ts +5 -0
  117. package/build/runtime/contract.js +108 -0
  118. package/build/runtime/fingerprint.d.ts +1 -0
  119. package/build/runtime/fingerprint.js +44 -0
  120. package/build/runtime/index.d.ts +2 -0
  121. package/build/runtime/index.js +50 -0
  122. package/build/start/index.d.ts +2 -0
  123. package/build/start/index.js +38 -0
  124. package/build/start/options.d.ts +9 -0
  125. package/build/start/options.js +38 -0
  126. package/build/updates/export.d.ts +14 -0
  127. package/build/updates/export.js +46 -0
  128. package/build/updates/index.d.ts +2 -0
  129. package/build/updates/index.js +96 -0
  130. package/build/upstream.d.ts +9 -0
  131. package/build/upstream.js +17 -0
  132. package/package.json +21 -5
  133. package/src/bin/expo-harmony.ts +2 -2
  134. package/src/buildHap/build.ts +13 -105
  135. package/src/buildHap/index.ts +19 -0
  136. package/src/cli.ts +27 -225
  137. package/src/command.ts +24 -0
  138. package/src/development/manifest.ts +106 -0
  139. package/src/development/protocol.ts +37 -0
  140. package/src/development/session.ts +83 -0
  141. package/src/doctor/doctor.ts +67 -62
  142. package/src/doctor/index.ts +16 -0
  143. package/src/exportEmbed/export.ts +73 -72
  144. package/src/exportEmbed/index.ts +17 -0
  145. package/src/file.ts +20 -11
  146. package/src/help.ts +41 -0
  147. package/src/index.ts +5 -3
  148. package/src/internal/development.ts +8 -0
  149. package/src/internal/export.ts +82 -0
  150. package/src/internal/prebuild.ts +3 -0
  151. package/src/log.ts +5 -0
  152. package/src/modules/index.ts +16 -0
  153. package/src/native/bare.ts +145 -0
  154. package/src/native/build.ts +83 -0
  155. package/src/{run → native}/cache.ts +10 -9
  156. package/src/native/hvigor.ts +88 -0
  157. package/src/native/install.ts +17 -0
  158. package/src/native/prepareProject.ts +99 -0
  159. package/src/native/project.ts +88 -0
  160. package/src/{tools.ts → native/toolchain.ts} +38 -140
  161. package/src/native/types.ts +44 -0
  162. package/src/prebuild/check.ts +56 -31
  163. package/src/prebuild/clean.ts +3 -3
  164. package/src/prebuild/index.ts +56 -0
  165. package/src/prebuild/prebuild.ts +11 -1
  166. package/src/prebuild/template.ts +2 -2
  167. package/src/process.ts +175 -64
  168. package/src/profile.ts +13 -0
  169. package/src/projectLock.ts +9 -1
  170. package/src/run/devices.ts +16 -11
  171. package/src/run/emulators.ts +4 -6
  172. package/src/run/index.ts +25 -0
  173. package/src/run/metro.ts +10 -1
  174. package/src/run/options.ts +2 -0
  175. package/src/run/run.ts +63 -129
  176. package/src/runtime/config.ts +150 -0
  177. package/src/runtime/contract.ts +165 -0
  178. package/src/runtime/fingerprint.ts +39 -0
  179. package/src/runtime/index.ts +54 -0
  180. package/src/start/index.ts +37 -0
  181. package/src/start/options.ts +12 -0
  182. package/src/updates/export.ts +60 -0
  183. package/src/updates/index.ts +89 -0
  184. package/src/buildHap/common.ts +0 -145
  185. package/src/run/install.ts +0 -41
package/src/run/metro.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import http from 'node:http';
2
2
 
3
+ import { readDevelopmentSessionAsync } from '../development/session';
3
4
  import { HarmonyCliError } from '../errors';
4
5
  import { formatDiagnostics, startManagedProcess, type ProcessResult } from '../process';
5
6
  import { resolveExpoCli } from '../expo';
@@ -8,13 +9,16 @@ type MetroStatus = 'free' | 'metro' | 'occupied';
8
9
 
9
10
  interface MetroOptions {
10
11
  interactive?: boolean;
12
+ host?: string;
11
13
  port: number;
14
+ privateKeyPath?: string;
12
15
  readyTimeoutMs?: number;
13
16
  resetCache?: boolean;
14
17
  }
15
18
 
16
19
  export interface MetroSession {
17
20
  owner: 'existing' | 'started';
21
+ development?: Awaited<ReturnType<typeof readDevelopmentSessionAsync>>;
18
22
  port: number;
19
23
  process?: ReturnType<typeof startManagedProcess>;
20
24
  stop(): Promise<unknown>;
@@ -101,6 +105,7 @@ async function startExpoMetroAsync(
101
105
 
102
106
  if (before === 'metro') {
103
107
  return {
108
+ development: await readDevelopmentSessionAsync(projectRoot, options.port, options.host),
104
109
  owner: 'existing',
105
110
  port: options.port,
106
111
  stop: async () => {},
@@ -118,17 +123,20 @@ async function startExpoMetroAsync(
118
123
 
119
124
  const expo = resolveExpoCli(projectRoot);
120
125
  const managed = startManagedProcess(process.execPath, [
126
+ require.resolve('../internal/development'),
121
127
  expo.cliPath,
122
128
  'start',
123
129
  projectRoot,
124
130
  '--dev-client',
125
131
  '--port', String(options.port),
132
+ ...(options.privateKeyPath ? ['--private-key-path', options.privateKeyPath] : []),
126
133
  ...(options.resetCache ? ['--clear'] : []),
127
134
  ], {
128
135
  cwd: projectRoot,
129
136
  env: {
130
137
  ...process.env,
131
138
  EXPO_METRO_TARGET: 'harmony',
139
+ ...(options.host ? { REACT_NATIVE_PACKAGER_HOSTNAME: options.host } : {}),
132
140
  },
133
141
  operation: 'expo-metro',
134
142
  outputLimit: 1024 * 1024,
@@ -147,7 +155,7 @@ async function startExpoMetroAsync(
147
155
  );
148
156
 
149
157
  const startedAt = Date.now();
150
- const timeoutMs = options.readyTimeoutMs || 60_000;
158
+ const timeoutMs = options.readyTimeoutMs || 1800000;
151
159
 
152
160
  try {
153
161
  while (Date.now() - startedAt < timeoutMs) {
@@ -184,6 +192,7 @@ async function startExpoMetroAsync(
184
192
 
185
193
  if (await probeMetroAsync(options.port) === 'metro') {
186
194
  return {
195
+ development: await readDevelopmentSessionAsync(projectRoot, options.port, options.host),
187
196
  owner: 'started',
188
197
  port: options.port,
189
198
  process: managed,
@@ -8,6 +8,7 @@ const RunOptions = {
8
8
  'no-bundler': { type: 'boolean' },
9
9
  'no-install': { type: 'boolean' },
10
10
  'port': { type: 'string' },
11
+ 'private-key-path': { type: 'string' },
11
12
  'reset-cache': { type: 'boolean' },
12
13
  'sync': { type: 'boolean' },
13
14
  'variant': { type: 'string' },
@@ -61,6 +62,7 @@ function parseRunArgs(argv: string[]) {
61
62
  noBundler: Boolean(values['no-bundler']),
62
63
  noInstall: Boolean(values['no-install']),
63
64
  port,
65
+ privateKeyPath: values['private-key-path'],
64
66
  project: positionals[0],
65
67
  resetCache: Boolean(values['reset-cache']),
66
68
  sync: Boolean(values.sync),
package/src/run/run.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import path from 'node:path';
2
+ import { getConfig } from '@expo/config';
2
3
 
3
4
  import {
4
5
  configureMetroPortAsync,
@@ -7,33 +8,18 @@ import {
7
8
  selectDeviceAsync,
8
9
  } from './devices';
9
10
  import { HarmonyCliError } from '../errors';
10
- import { exportEmbedAsync } from '../exportEmbed/export';
11
- import type { HarmonyExportManifest } from '../exportEmbed/manifest';
12
- import {
13
- resolveHarmonyBuildPlanAsync,
14
- resolveHarmonyEmulator,
15
- resolveHarmonyToolchain,
16
- type HarmonyBuildPlan,
17
- } from '../tools';
18
- import { installHarmonyDependenciesAsync } from './install';
11
+ import { resolveHarmonyEmulator, resolveHarmonyToolchain } from '../native/toolchain';
12
+ import { type HarmonyBuildPlan } from '../native/types';
19
13
  import {
20
14
  requireExistingMetroAsync,
21
15
  startExpoMetroAsync,
22
16
  type MetroSession,
23
17
  } from './metro';
24
- import {
25
- commitHarmonyNativeBuildCacheAsync,
26
- prepareHarmonyNativeBuildCacheAsync,
27
- } from './cache';
28
- import { toPosixPath } from '../path';
29
18
  import { withHarmonyProjectLockAsync } from '../projectLock';
30
- import {
31
- ensureGeneratedProjectAsync,
32
- isNonEmptyRegularFile,
33
- progress,
34
- runCheckedAsync,
35
- timed,
36
- } from '../buildHap/common';
19
+ import { buildNativeAsync, type NativeBuildResult } from '../native/build';
20
+ import { prepareNativeProjectAsync } from '../native/prepareProject';
21
+ import { progress } from '../log';
22
+ import { timedAsync } from '../profile';
37
23
 
38
24
  export interface HarmonyRunOptions {
39
25
  appId?: string;
@@ -42,16 +28,15 @@ export interface HarmonyRunOptions {
42
28
  noBundler?: boolean;
43
29
  noInstall?: boolean;
44
30
  port?: number;
31
+ privateKeyPath?: string;
45
32
  resetCache?: boolean;
46
33
  sync?: boolean;
47
34
  variant?: 'debug' | 'release';
48
35
  }
49
36
 
50
- export interface HarmonyRunResult {
37
+ export interface HarmonyRunResult extends NativeBuildResult {
51
38
  bundleName: string;
52
39
  device: { id: string; transport: string };
53
- export: null | { assetCount: number; bundleSha256: string; sourceMapSha256: string };
54
- hapPath: string;
55
40
  installed: boolean;
56
41
  launched: true;
57
42
  metro: { owner: 'disabled' | 'existing' | 'started'; port: number };
@@ -62,7 +47,6 @@ export interface HarmonyRunResult {
62
47
  }
63
48
 
64
49
  interface HarmonyRunSessionOptions extends HarmonyRunOptions {
65
- /** Give a CLI-started Metro process ownership of the current terminal. */
66
50
  interactiveBundler?: boolean;
67
51
  }
68
52
 
@@ -76,8 +60,8 @@ interface HarmonyRunSession {
76
60
  result: HarmonyRunResult;
77
61
  }
78
62
 
79
- type NormalizedRunOptions = Required<Omit<HarmonyRunSessionOptions, 'appId' | 'device'>>
80
- & Pick<HarmonyRunSessionOptions, 'appId' | 'device'>;
63
+ type NormalizedRunOptions = Required<Omit<HarmonyRunSessionOptions, 'appId' | 'device' | 'privateKeyPath'>>
64
+ & Pick<HarmonyRunSessionOptions, 'appId' | 'device' | 'privateKeyPath'>;
81
65
 
82
66
  const BundleName = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*){2,}$/u;
83
67
 
@@ -101,10 +85,10 @@ function resolveRunIdentity(plan: HarmonyBuildPlan, options: NormalizedRunOption
101
85
  }
102
86
 
103
87
  async function runHarmonyUnlockedAsync(
104
- projectRoot: string,
88
+ root: string,
105
89
  options: HarmonyRunSessionOptions = {}
106
90
  ): Promise<HarmonyRunSession> {
107
- const normalizedOptions: NormalizedRunOptions = {
91
+ const settings: NormalizedRunOptions = {
108
92
  appId: options.appId,
109
93
  device: options.device,
110
94
  interactiveBundler: Boolean(options.interactiveBundler),
@@ -112,108 +96,60 @@ async function runHarmonyUnlockedAsync(
112
96
  noBundler: Boolean(options.noBundler),
113
97
  noInstall: Boolean(options.noInstall),
114
98
  port: options.port || 8081,
99
+ privateKeyPath: options.privateKeyPath,
115
100
  resetCache: Boolean(options.resetCache),
116
101
  sync: Boolean(options.sync),
117
102
  variant: options.variant || 'debug',
118
103
  };
119
104
  const steps: Record<string, number> = {};
120
105
 
121
- await ensureGeneratedProjectAsync(projectRoot, normalizedOptions, steps);
106
+ const plan = await prepareNativeProjectAsync(root, {
107
+ ...settings,
108
+ check: false,
109
+ }, steps);
122
110
 
123
- const plan = await timed(steps, 'buildPlan', () => resolveHarmonyBuildPlanAsync(
124
- projectRoot,
125
- { buildMode: normalizedOptions.variant }
126
- ));
127
- const identity = resolveRunIdentity(plan, normalizedOptions);
128
- const toolchain = resolveHarmonyToolchain();
111
+ const identity = resolveRunIdentity(plan, settings);
112
+ const tools = resolveHarmonyToolchain();
129
113
 
130
- progress(normalizedOptions, 'Selecting a Harmony device or emulator');
131
- const device = await timed(steps, 'device', () => selectDeviceAsync(
132
- toolchain.hdc,
133
- normalizedOptions.device,
114
+ progress(settings, 'Selecting a Harmony device or emulator');
115
+ const device = await timedAsync(steps, 'device', () => selectDeviceAsync(
116
+ tools.hdc,
117
+ settings.device,
134
118
  {
135
119
  cwd: plan.harmonyRoot,
136
- emulator: resolveHarmonyEmulator(toolchain),
137
- emulatorLogFile: path.join(projectRoot, '.expo', 'harmony', 'emulator.log'),
138
- onProgress: message => progress(normalizedOptions, message),
120
+ emulator: resolveHarmonyEmulator(tools),
121
+ emulatorLogFile: path.join(root, '.expo', 'harmony', 'emulator.log'),
122
+ onProgress: message => progress(settings, message),
139
123
  }
140
124
  ));
141
125
 
142
- let exportManifest: HarmonyExportManifest | null = null;
143
- if (normalizedOptions.variant === 'release') {
144
- progress(normalizedOptions, 'Exporting the release Hermes bundle');
145
- exportManifest = await timed(steps, 'export', () => exportEmbedAsync(
146
- projectRoot,
147
- { resetCache: normalizedOptions.resetCache, skipDoctor: true }
148
- ));
149
- } else {
150
- steps.export = 0;
151
- }
152
-
153
- progress(normalizedOptions, 'Installing Harmony project dependencies');
154
- await timed(steps, 'ohpm', () => installHarmonyDependenciesAsync(plan, toolchain));
126
+ const built = await buildNativeAsync(root, plan, tools, settings, steps);
155
127
 
156
128
  let metro: HarmonyRunSession['metro'] = {
157
129
  owner: 'disabled',
158
- port: normalizedOptions.port,
130
+ port: settings.port,
159
131
  stop: async () => {},
160
132
  waitAsync: async () => {},
161
133
  };
162
- try {
163
- progress(normalizedOptions, 'Checking Harmony native dependency cache');
164
- const nativeBuildCache = await timed(steps, 'nativeCache', () => (
165
- prepareHarmonyNativeBuildCacheAsync(projectRoot, plan)
166
- ));
167
134
 
168
- if (nativeBuildCache.changed) {
169
- progress(normalizedOptions, 'Invalidated stale Harmony native build objects');
170
- }
171
-
172
- progress(normalizedOptions, `Building the ${normalizedOptions.variant} HAP`);
173
- const buildEnv = {
174
- ...process.env,
175
- EXPO_HARMONY_NODE: process.env.EXPO_HARMONY_NODE || process.execPath,
176
- EXPO_METRO_TARGET: 'harmony',
177
- HERMES_V1_ENABLED: 'true',
178
- ...(normalizedOptions.variant === 'release' ? { EXPO_HARMONY_BUNDLE_PREBUILT: '1' } : {}),
179
- ...(toolchain.sdkHome && !process.env.DEVECO_SDK_HOME
180
- ? { DEVECO_SDK_HOME: toolchain.sdkHome }
181
- : {}),
182
- };
183
- await timed(steps, 'build', () => runCheckedAsync(toolchain.hvigor.command, [
184
- ...toolchain.hvigor.args,
185
- ...plan.hvigorArgs,
186
- ], {
187
- code: 'ERR_HARMONY_BUILD_FAILED',
188
- cwd: plan.harmonyRoot,
189
- env: buildEnv,
190
- message: 'Hvigor build',
191
- operation: 'hvigor-build',
192
- timeoutMs: 15 * 60_000,
193
- }));
194
-
195
- if (!isNonEmptyRegularFile(plan.expectedHap)) {
196
- throw new HarmonyCliError('ERR_HARMONY_HAP_MISSING', 'Hvigor completed without producing the expected non-empty regular HAP.', { operation: 'verify-hap' });
197
- }
198
-
199
- await timed(steps, 'nativeCacheCommit', () => commitHarmonyNativeBuildCacheAsync(nativeBuildCache));
200
-
201
- if (normalizedOptions.variant === 'debug') {
202
- progress(normalizedOptions, normalizedOptions.noBundler
135
+ try {
136
+ if (settings.variant === 'debug' && getConfig(root).exp.updates?.useNativeDebug !== true) {
137
+ progress(settings, settings.noBundler
203
138
  ? 'Connecting to the existing Expo Metro server'
204
139
  : 'Starting Expo Metro');
205
- metro = await timed(steps, 'metro', () => normalizedOptions.noBundler
206
- ? requireExistingMetroAsync(normalizedOptions.port)
207
- : startExpoMetroAsync(projectRoot, {
208
- interactive: normalizedOptions.interactiveBundler,
209
- port: normalizedOptions.port,
210
- resetCache: normalizedOptions.resetCache,
140
+ metro = await timedAsync(steps, 'metro', () => settings.noBundler
141
+ ? requireExistingMetroAsync(settings.port)
142
+ : startExpoMetroAsync(root, {
143
+ interactive: settings.interactiveBundler,
144
+ port: settings.port,
145
+ privateKeyPath: settings.privateKeyPath,
146
+ resetCache: settings.resetCache,
211
147
  }));
212
148
 
213
- await timed(steps, 'metroPort', () => configureMetroPortAsync(
214
- toolchain.hdc,
149
+ await timedAsync(steps, 'metroPort', () => configureMetroPortAsync(
150
+ tools.hdc,
215
151
  device,
216
- normalizedOptions.port,
152
+ settings.port,
217
153
  { cwd: plan.harmonyRoot }
218
154
  ));
219
155
  } else {
@@ -221,21 +157,21 @@ async function runHarmonyUnlockedAsync(
221
157
  steps.metroPort = 0;
222
158
  }
223
159
 
224
- if (normalizedOptions.noInstall) {
160
+ if (settings.noInstall) {
225
161
  steps.install = 0;
226
162
  } else {
227
- progress(normalizedOptions, `Installing the HAP on ${device.id}`);
228
- await timed(steps, 'install', () => installHapAsync(
229
- toolchain.hdc,
163
+ progress(settings, `Installing the HAP on ${device.id}`);
164
+ await timedAsync(steps, 'install', () => installHapAsync(
165
+ tools.hdc,
230
166
  device,
231
167
  plan.expectedHap,
232
168
  { cwd: plan.harmonyRoot }
233
169
  ));
234
170
  }
235
171
 
236
- progress(normalizedOptions, `Launching ${identity.bundleName}`);
237
- await timed(steps, 'launch', () => launchAppAsync(
238
- toolchain.hdc,
172
+ progress(settings, `Launching ${identity.bundleName}`);
173
+ await timedAsync(steps, 'launch', () => launchAppAsync(
174
+ tools.hdc,
239
175
  device,
240
176
  identity.bundleName,
241
177
  identity.abilityName,
@@ -248,15 +184,8 @@ async function runHarmonyUnlockedAsync(
248
184
  id: device.id,
249
185
  transport: device.transport,
250
186
  },
251
- export: exportManifest
252
- ? {
253
- assetCount: exportManifest.assets.length,
254
- bundleSha256: exportManifest.bundle.sha256,
255
- sourceMapSha256: exportManifest.sourceMap.sha256,
256
- }
257
- : null,
258
- hapPath: toPosixPath(path.relative(projectRoot, plan.expectedHap)),
259
- installed: !normalizedOptions.noInstall,
187
+ ...built,
188
+ installed: !settings.noInstall,
260
189
  launched: true,
261
190
  metro: {
262
191
  owner: metro.owner,
@@ -265,32 +194,37 @@ async function runHarmonyUnlockedAsync(
265
194
  ok: true,
266
195
  schemaVersion: 1,
267
196
  steps,
268
- variant: normalizedOptions.variant,
197
+ variant: settings.variant,
269
198
  };
270
199
 
271
200
  return { metro, result };
272
201
  } catch (error) {
273
202
  await metro.stop();
274
- throw error;
203
+
204
+ throw new HarmonyCliError(error?.code || 'ERR_HARMONY_UNKNOWN', error?.message || 'Harmony run failed.', {
205
+ cause: error,
206
+ exitCode: error?.exitCode,
207
+ operation: error?.operation,
208
+ });
275
209
  }
276
210
  }
277
211
 
278
212
  async function runHarmonySessionAsync(
279
- projectRoot: string,
213
+ root: string,
280
214
  options: HarmonyRunSessionOptions = {}
281
215
  ): Promise<HarmonyRunSession> {
282
216
  return withHarmonyProjectLockAsync(
283
- projectRoot,
217
+ root,
284
218
  'run',
285
- () => runHarmonyUnlockedAsync(projectRoot, options)
219
+ () => runHarmonyUnlockedAsync(root, options)
286
220
  );
287
221
  }
288
222
 
289
223
  async function runHarmonyAsync(
290
- projectRoot: string,
224
+ root: string,
291
225
  options: HarmonyRunOptions = {}
292
226
  ): Promise<HarmonyRunResult> {
293
- const session = await runHarmonySessionAsync(projectRoot, options);
227
+ const session = await runHarmonySessionAsync(root, options);
294
228
 
295
229
  try {
296
230
  return session.result;
@@ -0,0 +1,150 @@
1
+ import fs from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+
5
+ import { getConfig } from '@expo/config';
6
+ import { compileHarmonyModsAsync, normalizeHarmonyConfig, type ExpoConfigWithHarmony } from '@expo-harmony/config-plugins';
7
+ import { withHarmonyPrebuildConfig } from '@expo-harmony/prebuild-config';
8
+ import type { HarmonyRuntimeContract } from '@expo-harmony/expo-modules-autolinking/runtime';
9
+ import JSON5 from 'json5';
10
+
11
+ import { fingerprintHarmonyAsync } from './fingerprint';
12
+ import { HarmonyCliError } from '../errors';
13
+ import { resolveHarmonyBuildPlanAsync } from '../native/project';
14
+ import type { HarmonyBuildPlan } from '../native/types';
15
+
16
+ export interface HarmonyMetroRuntime {
17
+ reactPackage: string;
18
+ harmonyPackage: string;
19
+ fixedRuntime: boolean;
20
+ }
21
+
22
+ export async function resolveRuntimeVersionAsync(root: string, config: ExpoConfigWithHarmony, app): Promise<string> {
23
+ if (typeof config.runtimeVersion === 'object' && config.runtimeVersion?.policy === 'nativeVersion') {
24
+ if (typeof app?.versionName !== 'string' || !app.versionName
25
+ || !Number.isSafeInteger(app.versionCode) || app.versionCode < 1) {
26
+ throw new HarmonyCliError(
27
+ 'ERR_HARMONY_RUNTIME_CONFIG',
28
+ 'The nativeVersion policy requires a Harmony versionName and a positive versionCode.',
29
+ { operation: 'runtime-contract' }
30
+ );
31
+ }
32
+
33
+ return `${app.versionName}(${app.versionCode})`;
34
+ }
35
+
36
+ if (typeof config.runtimeVersion === 'object' && config.runtimeVersion?.policy === 'fingerprint') {
37
+ return (await fingerprintHarmonyAsync(root)).hash;
38
+ }
39
+
40
+ const require = createRequire(path.join(root, 'package.json'));
41
+ const expo = createRequire(require.resolve('expo/package.json'));
42
+ const { getRuntimeVersionAsync } = expo('@expo/config-plugins/build/utils/Updates');
43
+ const version = await getRuntimeVersionAsync(root, {
44
+ ...config,
45
+ runtimeVersion: config.runtimeVersion ?? { policy: 'sdkVersion' },
46
+ }, 'harmony');
47
+
48
+ if (typeof version !== 'string' || !version) {
49
+ throw new HarmonyCliError(
50
+ 'ERR_HARMONY_RUNTIME_CONFIG',
51
+ 'Cannot resolve expo.runtimeVersion for Harmony.',
52
+ { operation: 'runtime-contract' }
53
+ );
54
+ }
55
+
56
+ return version;
57
+ }
58
+
59
+ export async function readMetroRuntimeAsync(root: string): Promise<HarmonyMetroRuntime | undefined> {
60
+ const require = createRequire(path.join(root, 'package.json'));
61
+ const previous = process.env.EXPO_METRO_TARGET;
62
+ let config;
63
+
64
+ try {
65
+ process.env.EXPO_METRO_TARGET = 'harmony';
66
+ config = (await require('metro-config').resolveConfig(undefined, root)).config;
67
+ } finally {
68
+ if (previous === undefined) delete process.env.EXPO_METRO_TARGET;
69
+ else process.env.EXPO_METRO_TARGET = previous;
70
+ }
71
+
72
+ return config?.resolver?.resolveRequest?.harmonyRuntime;
73
+ }
74
+
75
+ function nativeConfig(profile, module, build, product: string, ability: string): HarmonyRuntimeContract['config'] {
76
+ const selected = profile?.app?.products?.find(item => item.name === product);
77
+ const entry = module?.abilities?.find(item => item.name === ability);
78
+ if (!selected || !entry || !build
79
+ || /(?:USE_HERMES|HERMES_V1_ENABLED)(?::BOOL)?=(?:OFF|FALSE|0)/i.test(build.buildOption?.externalNativeOptions?.arguments || '')
80
+ || Object.keys(selected.buildOption || {}).some(key => key !== 'nativeCompiler')
81
+ || profile.app.buildModeSet?.some(mode => mode.buildOption)
82
+ || build.buildOptionSet?.length || build.targets?.some(target => target.buildOption)) {
83
+ throw new HarmonyCliError(
84
+ 'ERR_HARMONY_RUNTIME_CONFIG',
85
+ 'Runtime contracts require an existing Harmony product and ability, Hermes v1, and native options without per-target or per-mode overrides.',
86
+ { operation: 'runtime-contract' }
87
+ );
88
+ }
89
+
90
+ const api = (value: unknown) => typeof value === 'number' ? value : Number(/\((\d+)\)$/.exec(String(value))?.[1] ?? value);
91
+
92
+ return {
93
+ nativeCompiler: selected.buildOption?.nativeCompiler,
94
+ targetApiVersion: api(selected.targetSdkVersion),
95
+ compatibleApiVersion: api(selected.compatibleSdkVersion),
96
+ permissions: [...new Set<string>((module.requestPermissions || []).map(item => item.name))].sort(),
97
+ querySchemes: [...new Set<string>(module.querySchemes || [])].sort(),
98
+ backgroundModes: [...new Set<string>(entry.backgroundModes || [])].sort(),
99
+ abiFilters: [...(build.buildOption?.externalNativeOptions?.abiFilters || [])].sort(),
100
+ };
101
+ }
102
+
103
+ export function readNativeRuntime(plan: HarmonyBuildPlan) {
104
+ const read = (file: string) => JSON5.parse(fs.readFileSync(file, 'utf8'));
105
+ const app = read(path.join(plan.harmonyRoot, 'AppScope/app.json5')).app;
106
+ const profile = read(plan.projectFiles.projectBuildProfile);
107
+ const module = read(plan.projectFiles.moduleJson).module;
108
+ const build = read(path.join(plan.moduleRoot, 'build-profile.json5'));
109
+
110
+ return { app, config: nativeConfig(profile, module, build, plan.productName, plan.abilityName) };
111
+ }
112
+
113
+ export async function readProjectRuntimeAsync(root: string) {
114
+ const { exp } = getConfig(root, { skipSDKVersionRequirement: true, isModdedConfig: true });
115
+ const config = exp as ExpoConfigWithHarmony;
116
+ let native: ReturnType<typeof readNativeRuntime>;
117
+ let bundle: string;
118
+
119
+ if (config.harmony) {
120
+ const settings = normalizeHarmonyConfig(config);
121
+ const evaluated = await compileHarmonyModsAsync(withHarmonyPrebuildConfig(config), {
122
+ projectRoot: root,
123
+ introspect: true,
124
+ ignoreExistingNativeFiles: true,
125
+ });
126
+ const results = evaluated._internal?.modResults?.harmony;
127
+ native = {
128
+ app: results?.appJson?.app,
129
+ config: nativeConfig(
130
+ results?.projectBuildProfile,
131
+ results?.moduleJson?.module,
132
+ results?.entryBuildProfile,
133
+ settings.productName,
134
+ settings.abilityName
135
+ ),
136
+ };
137
+ bundle = settings.bundleName;
138
+ } else {
139
+ const plan = await resolveHarmonyBuildPlanAsync(root);
140
+ native = readNativeRuntime(plan);
141
+ bundle = plan.bundleName;
142
+ }
143
+
144
+ return {
145
+ config,
146
+ bundleName: bundle,
147
+ native: native.config,
148
+ runtimeVersion: await resolveRuntimeVersionAsync(root, config, native.app),
149
+ };
150
+ }