@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
@@ -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/native-project';
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
- resolveHarmonyBuildPlanIfPresentAsync,
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, packageName) {
41
- return (plugins || []).some(plugin => (Array.isArray(plugin) ? plugin[0] : plugin) === packageName);
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(projectRoot, packageName) {
49
- const projectRequire = createRequire(path.join(projectRoot, 'package.json'));
45
+ function canResolvePackage(root, name) {
46
+ const require = createRequire(path.join(root, 'package.json'));
47
+
50
48
  try {
51
- projectRequire.resolve(`${packageName}/package.json`);
49
+ require.resolve(`${name}/package.json`);
52
50
  return true;
53
51
  } catch {
54
52
  try {
55
- projectRequire.resolve(packageName);
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(projectRoot) {
64
- const projectRequire = createRequire(path.join(projectRoot, 'package.json'));
65
- let metroConfig;
61
+ async function validateMetroConfigAsync(root) {
62
+ const require = createRequire(path.join(root, 'package.json'));
63
+ let metro;
66
64
 
67
65
  try {
68
- metroConfig = projectRequire('metro-config');
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 metroConfig.resolveConfig !== 'function') {
71
+ if (typeof metro.resolveConfig !== 'function') {
74
72
  const version = (() => {
75
73
  try {
76
- return projectRequire('metro-config/package.json').version;
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 hadMetroTarget = Object.hasOwn(process.env, 'EXPO_METRO_TARGET');
86
- const previousMetroTarget = process.env.EXPO_METRO_TARGET;
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 metroConfig.resolveConfig(undefined, projectRoot);
89
+ resolved = await metro.resolveConfig(undefined, root);
92
90
  } finally {
93
- if (hadMetroTarget) process.env.EXPO_METRO_TARGET = previousMetroTarget;
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(projectRoot: string, options: DoctorOptions = {}): Promise<DoctorResult> {
106
+ async function doctorUnlockedAsync(root: string, options: DoctorOptions = {}): Promise<DoctorResult> {
109
107
  const checks: DoctorCheck[] = [];
110
- const unavailableToolStatus = options.requireBuildTools ? 'error' : 'warn';
108
+ const unavailable = options.requireBuildTools ? 'error' : 'warn';
111
109
  let config;
110
+ const bare = isBareHarmonyProject(root);
112
111
 
113
112
  try {
114
- config = getConfig(projectRoot, {
115
- isModdedConfig: true,
116
- skipSDKVersionRequirement: true,
117
- }).exp;
118
- normalizeHarmonyConfig(config);
119
- checks.push(check('app-config', 'pass', 'Harmony app config is valid.'));
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(projectRoot, harmony.signingConfigFile);
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(projectRoot);
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 packageName of RequiredProjectPackages) {
152
- if (canResolvePackage(projectRoot, packageName)) {
153
- checks.push(check(`package:${packageName}`, 'pass', `${packageName} is resolvable.`));
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:${packageName}`, 'error', `${packageName} is not resolvable from the app.`));
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 sdkCheck;
198
+ let sdk;
194
199
 
195
200
  if (toolchain.sdkHome) {
196
- sdkCheck = check('harmony-sdk', 'pass', `Complete Harmony SDK root was resolved at ${toolchain.sdkHome}.`);
201
+ sdk = check('harmony-sdk', 'pass', `Complete Harmony SDK root was resolved at ${toolchain.sdkHome}.`);
197
202
  } else {
198
- sdkCheck = check('harmony-sdk', unavailableToolStatus, 'No complete Harmony SDK root with HMS and OpenHarmony components was found; generation works but HAP build cannot be verified.');
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(sdkCheck);
206
+ checks.push(sdk);
202
207
 
203
208
  const tools: Array<[string, HarmonyTool, string[], DoctorCheck['status']]> = [
204
- ['ohpm', toolchain.ohpm, ['--version'], unavailableToolStatus],
205
- ['hvigor-command', toolchain.hvigor, ['--version'], unavailableToolStatus],
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'], unavailableToolStatus]);
213
+ tools.unshift(['hdc', toolchain.hdc, ['-v'], unavailable]);
209
214
  }
210
- for (const [id, tool, versionArgs, unavailableStatus] of tools) {
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, ...versionArgs], {
220
+ const result = await spawnAsync(tool.command, [...tool.args, ...args], {
213
221
  capture: true,
214
- cwd: projectRoot,
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', `${tool.command} is available through ${tool.source}.`)
220
- : check(id, unavailableStatus, `${tool.command} is unavailable or unhealthy; HAP build cannot be verified.`));
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, unavailableStatus, `${tool.command} is unavailable; generation remains available.`));
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(projectRoot);
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', 'Generated root and module Hvigor files are required.'));
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 generated module Hvigor file must disable RNOH autolinking.'));
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(projectRoot: string, options: DoctorOptions = {}): Promise<DoctorResult> {
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 { doctorAsync, type DoctorResult } from '../doctor/doctor';
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 '../tools';
11
+ import { resolveHarmonyBuildPlanAsync } from '../native/project';
11
12
  import { formatDiagnostics, spawnAsync } from '../process';
12
- import { resolveExpoCli, resolveExpoHermesBuilder } from '../expo';
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
- projectRoot: string,
38
+ root: string,
65
39
  options: ExportOptions = {}
66
40
  ): Promise<HarmonyExportManifest> {
67
- if (!options.skipDoctor) assertDoctor(await doctorAsync(projectRoot));
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(projectRoot, { buildMode: 'release' });
70
- const paths = exportPaths(plan);
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 entryFile = resolveHarmonyEntryPoint(projectRoot);
75
- const temporaryRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'expo-harmony-export-'));
76
- const temporary: ExportTemporary = {
77
- assets: path.join(temporaryRoot, 'assets'),
78
- bundle: path.join(temporaryRoot, 'hermes_bundle.hbc'),
79
- javascript: path.join(temporaryRoot, 'index.js'),
80
- metroSourceMap: path.join(temporaryRoot, 'index.js.map'),
81
- sourceMap: path.join(temporaryRoot, 'hermes_bundle.hbc.map'),
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(temporary.assets, { recursive: true });
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
- expo.cliPath,
90
- ...createExpoExportEmbedArgs(projectRoot, entryFile, temporary, options),
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: projectRoot,
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(temporary.javascript, 'utf8'),
119
- fs.promises.readFile(temporary.metroSourceMap, 'utf8'),
108
+ fs.promises.readFile(temp.javascript, 'utf8'),
109
+ fs.promises.readFile(temp.metroSourceMap, 'utf8'),
120
110
  ]);
121
- const buildHermesBundleAsync = resolveExpoHermesBuilder(projectRoot);
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 projectRequire = createRequire(path.join(projectRoot, 'package.json'));
125
- const harmonyRuntime = path.dirname(projectRequire.resolve('@react-native-oh/react-native-harmony/package.json'));
126
- const compilerModules = path.join(temporaryRoot, 'node_modules');
127
- await fs.promises.mkdir(compilerModules);
128
- await fs.promises.symlink(harmonyRuntime, path.join(compilerModules, 'react-native'),
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: entryFile,
125
+ filename: entry,
133
126
  map,
134
127
  minify: true,
135
- projectRoot: temporaryRoot,
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(temporary.bundle, output.hbc),
144
- fs.promises.writeFile(temporary.sourceMap, output.sourcemap),
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) throw cause;
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(temporary.bundle);
156
- await assertSourceMap(temporary.sourceMap);
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 await publishExportAsync(projectRoot, paths, temporary, entryFile, bytecode);
161
+ return manifest;
159
162
  } finally {
160
- await fs.promises.rm(temporaryRoot, { force: true, recursive: true });
163
+ await fs.promises.rm(directory, { force: true, recursive: true });
161
164
  }
162
165
  }
163
166
 
164
167
  async function exportEmbedAsync(
165
- projectRoot: string,
168
+ root: string,
166
169
  options: ExportOptions = {}
167
170
  ): Promise<HarmonyExportManifest> {
168
171
  return withHarmonyProjectLockAsync(
169
- projectRoot,
172
+ root,
170
173
  'export:embed',
171
- () => exportEmbedUnlockedAsync(projectRoot, options)
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 nodeCrypto from 'node:crypto';
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 = nodeCrypto.createHash('sha256');
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 temporary = path.join(path.dirname(destination), `.${path.basename(destination)}.expo-${nodeCrypto.randomUUID()}.tmp`);
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, temporary, fs.constants.COPYFILE_EXCL);
77
- await fs.promises.rename(temporary, destination);
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(temporary, { force: true });
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 temporary = path.join(path.dirname(destination), `.${path.basename(destination)}.expo-${nodeCrypto.randomUUID()}.tmp`);
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(temporary, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx' });
90
- await fs.promises.rename(temporary, destination);
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(temporary, { force: true });
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
- export { atomicCopy, atomicWriteJson, describeFile, listFiles, removeEmptyParents };
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
+ `;