@expo-harmony/cli 55.0.26-harmony.8 → 55.0.26-harmony.9

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 (115) hide show
  1. package/README.md +74 -46
  2. package/build/bin/expo-harmony.js +1 -1
  3. package/build/buildHap/build.d.ts +3 -10
  4. package/build/buildHap/build.js +11 -69
  5. package/build/buildHap/index.d.ts +2 -0
  6. package/build/buildHap/index.js +19 -0
  7. package/build/cli.d.ts +3 -3
  8. package/build/cli.js +21 -187
  9. package/build/command.d.ts +9 -0
  10. package/build/command.js +20 -0
  11. package/build/doctor/doctor.d.ts +1 -1
  12. package/build/doctor/doctor.js +64 -56
  13. package/build/doctor/index.d.ts +2 -0
  14. package/build/doctor/index.js +16 -0
  15. package/build/exportEmbed/export.d.ts +2 -2
  16. package/build/exportEmbed/export.js +53 -49
  17. package/build/exportEmbed/index.d.ts +2 -0
  18. package/build/exportEmbed/index.js +19 -0
  19. package/build/file.d.ts +2 -1
  20. package/build/file.js +18 -8
  21. package/build/help.d.ts +1 -0
  22. package/build/help.js +36 -0
  23. package/build/index.d.ts +5 -3
  24. package/build/index.js +6 -5
  25. package/build/internal/prebuild.d.ts +3 -0
  26. package/build/internal/prebuild.js +9 -0
  27. package/build/log.d.ts +4 -0
  28. package/build/log.js +6 -0
  29. package/build/modules/index.d.ts +2 -0
  30. package/build/modules/index.js +16 -0
  31. package/build/native/bare.d.ts +4 -0
  32. package/build/native/bare.js +130 -0
  33. package/build/native/build.d.ts +15 -0
  34. package/build/native/build.js +67 -0
  35. package/build/{run → native}/cache.d.ts +2 -2
  36. package/build/{run → native}/cache.js +8 -8
  37. package/build/native/hvigor.d.ts +2 -0
  38. package/build/native/hvigor.js +77 -0
  39. package/build/native/install.d.ts +5 -0
  40. package/build/native/install.js +13 -0
  41. package/build/native/prepareProject.d.ts +11 -0
  42. package/build/native/prepareProject.js +68 -0
  43. package/build/native/project.d.ts +8 -0
  44. package/build/native/project.js +58 -0
  45. package/build/native/toolchain.d.ts +16 -0
  46. package/build/{tools.js → native/toolchain.js} +1 -57
  47. package/build/native/types.d.ts +47 -0
  48. package/build/native/types.js +2 -0
  49. package/build/prebuild/check.d.ts +7 -3
  50. package/build/prebuild/check.js +26 -20
  51. package/build/prebuild/clean.js +5 -5
  52. package/build/prebuild/index.d.ts +2 -0
  53. package/build/prebuild/index.js +51 -0
  54. package/build/prebuild/prebuild.js +11 -3
  55. package/build/prebuild/template.js +2 -2
  56. package/build/process.d.ts +11 -1
  57. package/build/process.js +16 -0
  58. package/build/profile.d.ts +1 -0
  59. package/build/profile.js +12 -0
  60. package/build/projectLock.js +7 -2
  61. package/build/run/devices.d.ts +1 -1
  62. package/build/run/devices.js +15 -10
  63. package/build/run/emulators.d.ts +1 -1
  64. package/build/run/emulators.js +2 -5
  65. package/build/run/index.d.ts +2 -0
  66. package/build/run/index.js +25 -0
  67. package/build/run/run.d.ts +4 -10
  68. package/build/run/run.js +45 -90
  69. package/build/start/index.d.ts +2 -0
  70. package/build/start/index.js +33 -0
  71. package/package.json +14 -4
  72. package/src/bin/expo-harmony.ts +2 -2
  73. package/src/buildHap/build.ts +13 -105
  74. package/src/buildHap/index.ts +19 -0
  75. package/src/cli.ts +24 -227
  76. package/src/command.ts +24 -0
  77. package/src/doctor/doctor.ts +63 -61
  78. package/src/doctor/index.ts +16 -0
  79. package/src/exportEmbed/export.ts +64 -55
  80. package/src/exportEmbed/index.ts +17 -0
  81. package/src/file.ts +20 -11
  82. package/src/help.ts +33 -0
  83. package/src/index.ts +5 -3
  84. package/src/internal/prebuild.ts +3 -0
  85. package/src/log.ts +5 -0
  86. package/src/modules/index.ts +16 -0
  87. package/src/native/bare.ts +145 -0
  88. package/src/native/build.ts +81 -0
  89. package/src/{run → native}/cache.ts +10 -9
  90. package/src/native/hvigor.ts +81 -0
  91. package/src/native/install.ts +17 -0
  92. package/src/native/prepareProject.ts +99 -0
  93. package/src/native/project.ts +88 -0
  94. package/src/{tools.ts → native/toolchain.ts} +11 -118
  95. package/src/native/types.ts +44 -0
  96. package/src/prebuild/check.ts +30 -19
  97. package/src/prebuild/clean.ts +3 -3
  98. package/src/prebuild/index.ts +56 -0
  99. package/src/prebuild/prebuild.ts +11 -1
  100. package/src/prebuild/template.ts +2 -2
  101. package/src/process.ts +38 -1
  102. package/src/profile.ts +13 -0
  103. package/src/projectLock.ts +9 -1
  104. package/src/run/devices.ts +16 -11
  105. package/src/run/emulators.ts +4 -6
  106. package/src/run/index.ts +25 -0
  107. package/src/run/run.ts +56 -129
  108. package/src/start/index.ts +31 -0
  109. package/build/buildHap/common.d.ts +0 -24
  110. package/build/buildHap/common.js +0 -92
  111. package/build/run/install.d.ts +0 -7
  112. package/build/run/install.js +0 -26
  113. package/build/tools.d.ts +0 -39
  114. package/src/buildHap/common.ts +0 -145
  115. package/src/run/install.ts +0 -41
package/src/cli.ts CHANGED
@@ -1,245 +1,42 @@
1
- import path from 'node:path';
2
-
3
- import { parseBuildArgs } from './buildHap/options';
4
- import { parseDoctorArgs } from './doctor/options';
1
+ import type { Command } from './command';
5
2
  import { HarmonyCliError } from './errors';
6
- import { parseExportEmbedArgs } from './exportEmbed/options';
7
- import { parseModulesArgs } from './modules/options';
8
- import { parsePrebuildArgs } from './prebuild/options';
9
- import { resolveProject } from './project';
10
- import { withHarmonyProjectLockAsync } from './projectLock';
11
- import { parseRunArgs } from './run/options';
12
- import { parseStartArgs } from './start/options';
13
-
14
- const Help = `Usage: expo-harmony <command> [project] [options]
15
-
16
- Commands:
17
- start Start Expo Metro for Harmony development
18
- prebuild Generate the Harmony native project with Expo CNG
19
- prebuild --clean Safely recreate the managed Harmony directory
20
- prebuild --check Compare generated desired state without project writes
21
- build Build a HAP without selecting or contacting a device
22
- doctor Validate config, versions, Metro, RNOH, SDK, and signing
23
- modules list List native module candidates and their discovery source
24
- modules inspect Resolve Harmony metadata (--package narrows the result)
25
- modules verify Validate module config, registration conflicts and safe paths
26
- export:embed Export validated Hermes bytecode, assets, and a source map
27
- run Build, install, and launch the Harmony app
28
-
29
- Options:
30
- --no-install Skip dependency install (prebuild) or HAP install (run)
31
- --npm|--yarn|--pnpm|--bun
32
- Select the package manager used by prebuild dependency install
33
- --skip-dependency-update <packages>
34
- Preserve comma-separated dependency versions
35
- --device <id-or-name> Select an HDC target or start a local emulator by name
36
- --variant <mode> Build debug or release (default: debug)
37
- --no-bundler Use an already-running Expo Metro server
38
- --app-id <bundleName>
39
- Launch another installed app (requires --no-install when different)
40
- --port <number> Metro and device reverse port (default: 8081)
41
- --sync Re-run prebuild before building
42
- --check Validate an existing export without writing
43
- --reset-cache Reset Metro while exporting or starting
44
- -c, --clear Alias for --reset-cache (start)
45
- -h, --help Show this help
46
- `;
47
-
48
- type Invocation = { command: 'help' }
49
- | { command: 'build'; parsed: ReturnType<typeof parseBuildArgs>; projectRoot: string }
50
- | { command: 'doctor'; parsed: ReturnType<typeof parseDoctorArgs>; projectRoot: string }
51
- | { command: 'export:embed'; parsed: ReturnType<typeof parseExportEmbedArgs>; projectRoot: string }
52
- | { command: 'modules'; parsed: ReturnType<typeof parseModulesArgs>; projectRoot: string }
53
- | { command: 'prebuild'; parsed: ReturnType<typeof parsePrebuildArgs>; projectRoot: string }
54
- | { command: 'run'; parsed: ReturnType<typeof parseRunArgs>; projectRoot: string }
55
- | { command: 'start'; parsed: ReturnType<typeof parseStartArgs>; projectRoot: string };
56
-
57
- function parseInvocation(argv: string[]): Invocation {
58
- if (argv.length === 0 || (argv.length === 1 && ['--help', '-h'].includes(argv[0]))) {
59
- return { command: 'help' };
60
- }
61
-
62
- const command = argv[0] as Exclude<Invocation['command'], 'help'>;
63
- if (!['build', 'prebuild', 'doctor', 'export:embed', 'modules', 'run', 'start'].includes(command)) {
64
- throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', `Unknown command: ${command}`, {
65
- operation: 'parse-arguments',
66
- });
67
- }
68
-
69
- const parsed = command === 'build'
70
- ? parseBuildArgs(argv.slice(1))
71
- : command === 'prebuild'
72
- ? parsePrebuildArgs(argv.slice(1), { allowProject: true })
73
- : command === 'doctor'
74
- ? parseDoctorArgs(argv.slice(1))
75
- : command === 'export:embed'
76
- ? parseExportEmbedArgs(argv.slice(1))
77
- : command === 'modules'
78
- ? parseModulesArgs(argv.slice(1))
79
- : command === 'start'
80
- ? parseStartArgs(argv.slice(1))
81
- : parseRunArgs(argv.slice(1));
82
- if (parsed.help) return { command: 'help' };
83
-
84
- const projectRoot = resolveProject(parsed.project ? path.resolve(parsed.project) : process.cwd());
85
-
86
- return { command, parsed, projectRoot } as Invocation;
87
- }
88
-
89
- async function runAsync(
90
- argv: string[] = process.argv.slice(2),
91
- io: Pick<Console, 'error' | 'log' | 'warn'> = console
92
- ): Promise<number> {
3
+ import { Help } from './help';
4
+ import type { Log } from './log';
5
+
6
+ const commands: Record<string, () => Promise<{ command: Command }>> = {
7
+ 'start': () => import('./start/index.js'),
8
+ 'prebuild': () => import('./prebuild/index.js'),
9
+ 'build': () => import('./buildHap/index.js'),
10
+ 'doctor': () => import('./doctor/index.js'),
11
+ 'modules': () => import('./modules/index.js'),
12
+ 'export:embed': () => import('./exportEmbed/index.js'),
13
+ 'run': () => import('./run/index.js'),
14
+ };
15
+
16
+ export async function runAsync(argv: string[] = process.argv.slice(2), io: Log = console): Promise<number> {
93
17
  process.env.EXPO_HARMONY = '1';
94
18
  process.env.EXPO_METRO_TARGET = 'harmony';
95
19
 
96
- const invocation = parseInvocation(argv);
97
- if (invocation.command === 'help') {
20
+ if (argv.length === 0 || (argv.length === 1 && ['--help', '-h'].includes(argv[0]))) {
98
21
  io.log(Help);
99
22
  return 0;
100
23
  }
101
24
 
102
- if (invocation.command === 'start') {
103
- const { startExpoMetroAsync } = await import('./run/metro.js');
104
- const metro = await startExpoMetroAsync(invocation.projectRoot, {
105
- ...invocation.parsed,
106
- interactive: true,
107
- });
108
-
109
- try {
110
- if (metro.owner === 'existing') {
111
- io.log(`Expo Metro is already running on port ${metro.port}.`);
112
- if (invocation.parsed.resetCache) {
113
- io.warn('Stop the existing Metro server and run this command again to reset its cache.');
114
- }
115
- } else {
116
- io.log('\n› Logs for your project will appear below. Press Ctrl+C to exit.');
117
- await metro.waitAsync();
118
- }
119
-
120
- return 0;
121
- } finally {
122
- await metro.stop();
123
- }
124
- }
125
-
126
- if (invocation.command === 'doctor') {
127
- const { doctorAsync, formatDoctor } = await import('./doctor/doctor.js');
128
- const result = await doctorAsync(invocation.projectRoot, { requireBuildTools: true });
129
-
130
- io.log(formatDoctor(result));
131
-
132
- return result.ok ? 0 : 1;
133
- }
134
-
135
- if (invocation.command === 'build') {
136
- const { buildHarmonyAsync } = await import('./buildHap/build.js');
137
- const result = await buildHarmonyAsync(invocation.projectRoot, {
138
- ...invocation.parsed,
139
- io,
140
- });
141
-
142
- io.log(`Built ${result.variant} HAP at ${result.hapPath}; no device actions were performed.`);
143
-
144
- return 0;
145
- }
146
-
147
- if (invocation.command === 'export:embed') {
148
- const { exportEmbedAsync } = await import('./exportEmbed/export.js');
149
- const result = await exportEmbedAsync(invocation.projectRoot, invocation.parsed);
150
-
151
- if (invocation.parsed.check) io.log('Harmony export bundle, assets, and source map are valid.');
152
- else io.log(`Exported Hermes bytecode ${result.bundle.path} with ${result.assets.length} asset file(s).`);
153
-
154
- return 0;
155
- }
156
-
157
- if (invocation.command === 'modules') {
158
- const { formatModulesResult, runModulesCommandAsync } = await import('./modules/modules.js');
159
- const result = await runModulesCommandAsync(invocation.projectRoot, invocation.parsed);
160
-
161
- io.log(formatModulesResult(result));
162
- return result.ok ? 0 : 1;
163
- }
164
-
165
- if (invocation.command === 'run') {
166
- const { runHarmonySessionAsync } = await import('./run/run.js');
167
-
168
- const session = await runHarmonySessionAsync(invocation.projectRoot, {
169
- ...invocation.parsed,
170
- interactiveBundler: true,
171
- io,
172
- });
173
- const { result } = session;
174
-
175
- io.log(`Launched ${result.bundleName} on ${result.device.id} (${result.variant}).`);
176
- if (session.metro.owner === 'started') {
177
- io.log('\n› Logs for your project will appear below. Press Ctrl+C to exit.');
178
- await session.metro.waitAsync();
179
- }
180
-
181
- return 0;
25
+ const name = argv[0];
26
+ if (!Object.hasOwn(commands, name)) {
27
+ throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', `Unknown command: ${name}`, { operation: 'parse-arguments' });
182
28
  }
183
29
 
184
- const { check, clean, passthrough } = invocation.parsed;
30
+ const { command } = await commands[name]();
185
31
 
186
- if (check && passthrough.length) {
187
- throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--check cannot be combined with mutating prebuild options.');
188
- }
189
-
190
- return withHarmonyProjectLockAsync(
191
- invocation.projectRoot,
192
- check ? 'prebuild-check' : 'prebuild',
193
- async () => {
194
- if (clean) {
195
- const { assertSafeCleanTarget } = await import('./prebuild/clean.js');
196
- await assertSafeCleanTarget(invocation.projectRoot);
197
- }
198
-
199
- const { doctorAsync, formatDoctor } = await import('./doctor/doctor.js');
200
- const doctor = await doctorAsync(invocation.projectRoot, {
201
- requireBuildTools: false,
202
- validateGeneratedProject: !clean,
203
- validateModules: false,
204
- });
205
- if (!doctor.ok) {
206
- io.error(formatDoctor(doctor));
207
- throw new HarmonyCliError('ERR_HARMONY_DOCTOR_FAILED', 'Harmony doctor found blocking errors.', {
208
- operation: 'doctor',
209
- });
210
- }
211
-
212
- for (const item of doctor.checks.filter(item => item.status === 'warn')) io.warn(`! ${item.message}`);
213
-
214
- if (check) {
215
- const { checkAsync } = await import('./prebuild/check.js');
216
- const result = await checkAsync(invocation.projectRoot);
217
-
218
- if (result.clean) io.log('Harmony CNG output is up to date.');
219
- else for (const change of result.changes) io.log(`${change.type}: ${change.path}`);
220
-
221
- return result.clean ? 0 : 2;
222
- }
223
-
224
- const { prebuildParsedAsync } = await import('./prebuild/prebuild.js');
225
- await prebuildParsedAsync(invocation.projectRoot, passthrough);
226
-
227
- return 0;
228
- });
32
+ return command(argv.slice(1), io);
229
33
  }
230
34
 
231
- async function main(argv = process.argv.slice(2)) {
35
+ export async function mainAsync(argv: string[] = process.argv.slice(2)): Promise<void> {
232
36
  try {
233
- const code = await runAsync(argv);
234
-
235
- process.exitCode = code;
37
+ process.exitCode = await runAsync(argv);
236
38
  } catch (error) {
237
- const code = error.code || 'ERR_HARMONY_UNKNOWN';
238
-
239
- console.error(`[${code}] ${error.message}`);
240
-
39
+ console.error(`[${error.code || 'ERR_HARMONY_UNKNOWN'}] ${error.message}`);
241
40
  process.exitCode = error.exitCode || 1;
242
41
  }
243
42
  }
244
-
245
- export { main, runAsync };
package/src/command.ts ADDED
@@ -0,0 +1,24 @@
1
+ import path from 'node:path';
2
+
3
+ import { Help } from './help';
4
+ import type { Log } from './log';
5
+ import { resolveProject } from './project';
6
+
7
+ export type Command = (argv: string[], io: Log) => Promise<number>;
8
+
9
+ export function parseCommandArgs<T extends { help: boolean; project?: string }>(
10
+ parse: (argv: string[]) => T,
11
+ argv: string[],
12
+ io: Log
13
+ ): { options: T; projectRoot: string } | null {
14
+ const options = parse(argv);
15
+ if (options.help) {
16
+ io.log(Help);
17
+ return null;
18
+ }
19
+
20
+ return {
21
+ options,
22
+ projectRoot: resolveProject(options.project ? path.resolve(options.project) : process.cwd()),
23
+ };
24
+ }
@@ -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,54 +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) {
211
217
  const command = [tool.command, ...tool.args].map(value => JSON.stringify(value)).join(' ');
212
218
 
213
219
  try {
214
- const result = await spawnAsync(tool.command, [...tool.args, ...versionArgs], {
220
+ const result = await spawnAsync(tool.command, [...tool.args, ...args], {
215
221
  capture: true,
216
- cwd: projectRoot,
222
+ cwd: root,
217
223
  operation: `doctor-${id}`,
218
224
  timeoutMs: 10_000,
219
225
  });
220
226
 
221
227
  checks.push(result.code === 0 && !result.timedOut
222
228
  ? check(id, 'pass', `${command} is available through ${tool.source}.`)
223
- : check(id, unavailableStatus, `${command} is unavailable or unhealthy; HAP build cannot be verified.`));
229
+ : check(id, status, `${command} is unavailable or unhealthy; HAP build cannot be verified.`));
224
230
  } catch {
225
- checks.push(check(id, unavailableStatus, `${command} is unavailable; generation remains available.`));
231
+ checks.push(check(id, status, `${command} is unavailable; generation remains available.`));
226
232
  }
227
233
  }
228
234
 
229
235
  if (options.validateGeneratedProject !== false) {
230
236
  try {
231
- const plan = await resolveHarmonyBuildPlanIfPresentAsync(projectRoot);
237
+ const plan = await resolveHarmonyBuildPlanIfPresentAsync(root);
232
238
  if (plan && fs.existsSync(plan.harmonyRoot)) {
233
239
  if (!fs.existsSync(plan.projectFiles.rootHvigor)
234
240
  || !fs.existsSync(plan.projectFiles.moduleHvigor)) {
235
- 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.'));
236
242
  } else {
237
243
  const content = await fs.promises.readFile(plan.projectFiles.moduleHvigor, 'utf8');
238
244
  checks.push(isRnohAutolinkingDisabled(content)
239
245
  ? check('hvigor-autolinking', 'pass', 'RNOH duplicate autolinking is disabled.')
240
- : 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.'));
241
247
  }
242
248
  }
243
249
  } catch (error) {
@@ -248,16 +254,12 @@ async function doctorUnlockedAsync(projectRoot: string, options: DoctorOptions =
248
254
  return {
249
255
  checks,
250
256
  ok: checks.every(item => item.status !== 'error'),
251
- projectRoot,
257
+ projectRoot: root,
252
258
  };
253
259
  }
254
260
 
255
- async function doctorAsync(projectRoot: string, options: DoctorOptions = {}): Promise<DoctorResult> {
256
- return withHarmonyProjectLockAsync(
257
- projectRoot,
258
- 'doctor',
259
- () => doctorUnlockedAsync(projectRoot, options)
260
- );
261
+ async function doctorAsync(root: string, options: DoctorOptions = {}): Promise<DoctorResult> {
262
+ return withHarmonyProjectLockAsync(root, 'doctor', () => doctorUnlockedAsync(root, options));
261
263
  }
262
264
 
263
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
+ };