@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.
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 +121 -0
  18. package/build/development/protocol.d.ts +3 -0
  19. package/build/development/protocol.js +33 -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 +223 -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 +37 -0
  115. package/build/runtime/config.js +169 -0
  116. package/build/runtime/contract.d.ts +5 -0
  117. package/build/runtime/contract.js +109 -0
  118. package/build/runtime/fingerprint.d.ts +1 -0
  119. package/build/runtime/fingerprint.js +49 -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 +127 -0
  139. package/src/development/protocol.ts +42 -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 +24 -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 +207 -0
  177. package/src/runtime/contract.ts +167 -0
  178. package/src/runtime/fingerprint.ts +49 -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
@@ -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,207 @@
1
+ import fs from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { createRequire } from 'node:module';
4
+ import path from 'node:path';
5
+
6
+ import { getConfig } from '@expo/config';
7
+ import { compileHarmonyModsAsync, normalizeHarmonyConfig, parseHarmonySdkVersion, type ExpoConfigWithHarmony } from '@expo-harmony/config-plugins';
8
+ import type { HarmonyRuntimeContract } from '@expo-harmony/expo-modules-autolinking/runtime';
9
+ import { withHarmonyPrebuildConfig } from '@expo-harmony/prebuild-config';
10
+ import JSON5 from 'json5';
11
+
12
+ import { fingerprintHarmonyAsync } from './fingerprint';
13
+ import { HarmonyCliError } from '../errors';
14
+ import { resolveHarmonyBuildPlanAsync } from '../native/project';
15
+ import type { HarmonyBuildPlan } from '../native/types';
16
+
17
+ export interface HarmonyMetroRuntime {
18
+ reactPackage: string;
19
+ harmonyPackage: string;
20
+ fixedRuntime: boolean;
21
+ }
22
+
23
+ export async function resolveRuntimeVersionAsync(root: string, config: ExpoConfigWithHarmony, app): Promise<string> {
24
+ if (typeof config.runtimeVersion === 'object' && config.runtimeVersion?.policy === 'nativeVersion') {
25
+ if (typeof app?.versionName !== 'string' || !app.versionName
26
+ || !Number.isSafeInteger(app.versionCode) || app.versionCode < 1) {
27
+ throw new HarmonyCliError(
28
+ 'ERR_HARMONY_RUNTIME_CONFIG',
29
+ 'The nativeVersion policy requires a Harmony versionName and a positive versionCode.',
30
+ { operation: 'runtime-contract' }
31
+ );
32
+ }
33
+
34
+ return `${app.versionName}(${app.versionCode})`;
35
+ }
36
+
37
+ if (typeof config.runtimeVersion === 'object' && config.runtimeVersion?.policy === 'fingerprint') {
38
+ return (await fingerprintHarmonyAsync(root)).hash;
39
+ }
40
+
41
+ const require = createRequire(path.join(root, 'package.json'));
42
+ const expo = createRequire(require.resolve('expo/package.json'));
43
+ const { getRuntimeVersionAsync } = expo('@expo/config-plugins/build/utils/Updates');
44
+ const version = await getRuntimeVersionAsync(root, {
45
+ ...config,
46
+ runtimeVersion: config.runtimeVersion ?? { policy: 'sdkVersion' },
47
+ }, 'harmony');
48
+
49
+ if (typeof version !== 'string' || !version) {
50
+ throw new HarmonyCliError(
51
+ 'ERR_HARMONY_RUNTIME_CONFIG',
52
+ 'Cannot resolve expo.runtimeVersion for Harmony.',
53
+ { operation: 'runtime-contract' }
54
+ );
55
+ }
56
+
57
+ return version;
58
+ }
59
+
60
+ export async function readMetroRuntimeAsync(root: string): Promise<HarmonyMetroRuntime | undefined> {
61
+ const require = createRequire(path.join(root, 'package.json'));
62
+ const previous = process.env.EXPO_METRO_TARGET;
63
+ let config;
64
+
65
+ try {
66
+ process.env.EXPO_METRO_TARGET = 'harmony';
67
+ config = (await require('metro-config').resolveConfig(undefined, root)).config;
68
+ } finally {
69
+ if (previous === undefined) delete process.env.EXPO_METRO_TARGET;
70
+ else process.env.EXPO_METRO_TARGET = previous;
71
+ }
72
+
73
+ return config?.resolver?.resolveRequest?.harmonyRuntime;
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
+
130
+ function nativeConfig(profile, module, build, product: string, ability: string): HarmonyRuntimeContract['config'] {
131
+ const selected = profile?.app?.products?.find(item => item.name === product);
132
+ const entry = module?.abilities?.find(item => item.name === ability);
133
+
134
+ if (!selected || !entry || !build
135
+ || /(?:USE_HERMES|HERMES_V1_ENABLED)(?::BOOL)?=(?:OFF|FALSE|0)/i.test(build.buildOption?.externalNativeOptions?.arguments || '')
136
+ || Object.keys(selected.buildOption || {}).some(key => key !== 'nativeCompiler')
137
+ || profile.app.buildModeSet?.some(mode => mode.buildOption)
138
+ || !supportedReleaseOptions(build.buildOptionSet) || build.targets?.some(target => target.buildOption)) {
139
+ throw new HarmonyCliError(
140
+ 'ERR_HARMONY_RUNTIME_CONFIG',
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.',
142
+ { operation: 'runtime-contract' }
143
+ );
144
+ }
145
+
146
+ const hash = nativeLibFilterHash(build.buildOption?.nativeLib?.filter);
147
+
148
+ return {
149
+ nativeCompiler: selected.buildOption?.nativeCompiler,
150
+ targetApiVersion: parseHarmonySdkVersion(selected.targetSdkVersion, 'targetSdkVersion').api,
151
+ compatibleApiVersion: parseHarmonySdkVersion(selected.compatibleSdkVersion, 'compatibleSdkVersion').api,
152
+ ...(hash ? { nativeLibFilterHash: hash } : {}),
153
+ permissions: [...new Set<string>((module.requestPermissions || []).map(item => item.name))].sort(),
154
+ querySchemes: [...new Set<string>(module.querySchemes || [])].sort(),
155
+ backgroundModes: [...new Set<string>(entry.backgroundModes || [])].sort(),
156
+ abiFilters: [...(build.buildOption?.externalNativeOptions?.abiFilters || [])].sort(),
157
+ };
158
+ }
159
+
160
+ export function readNativeRuntime(plan: HarmonyBuildPlan) {
161
+ const read = (file: string) => JSON5.parse(fs.readFileSync(file, 'utf8'));
162
+ const app = read(path.join(plan.harmonyRoot, 'AppScope/app.json5')).app;
163
+ const profile = read(plan.projectFiles.projectBuildProfile);
164
+ const module = read(plan.projectFiles.moduleJson).module;
165
+ const build = read(path.join(plan.moduleRoot, 'build-profile.json5'));
166
+
167
+ return { app, config: nativeConfig(profile, module, build, plan.productName, plan.abilityName) };
168
+ }
169
+
170
+ export async function readProjectRuntimeAsync(root: string) {
171
+ const { exp } = getConfig(root, { skipSDKVersionRequirement: true, isModdedConfig: true });
172
+ const config = exp as ExpoConfigWithHarmony;
173
+ let native: ReturnType<typeof readNativeRuntime>;
174
+ let bundle: string;
175
+
176
+ if (config.harmony) {
177
+ const settings = normalizeHarmonyConfig(config);
178
+ const evaluated = await compileHarmonyModsAsync(withHarmonyPrebuildConfig(config), {
179
+ projectRoot: root,
180
+ introspect: true,
181
+ ignoreExistingNativeFiles: true,
182
+ });
183
+ const results = evaluated._internal?.modResults?.harmony;
184
+ native = {
185
+ app: results?.appJson?.app,
186
+ config: nativeConfig(
187
+ results?.projectBuildProfile,
188
+ results?.moduleJson?.module,
189
+ results?.entryBuildProfile,
190
+ settings.productName,
191
+ settings.abilityName
192
+ ),
193
+ };
194
+ bundle = settings.bundleName;
195
+ } else {
196
+ const plan = await resolveHarmonyBuildPlanAsync(root);
197
+ native = readNativeRuntime(plan);
198
+ bundle = plan.bundleName;
199
+ }
200
+
201
+ return {
202
+ config,
203
+ bundleName: bundle,
204
+ native: native.config,
205
+ runtimeVersion: await resolveRuntimeVersionAsync(root, config, native.app),
206
+ };
207
+ }