@expo-harmony/cli 55.0.26-harmony.7 → 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.
- package/README.md +74 -46
- package/build/bin/expo-harmony.js +1 -1
- package/build/buildHap/build.d.ts +3 -10
- package/build/buildHap/build.js +11 -69
- package/build/buildHap/index.d.ts +2 -0
- package/build/buildHap/index.js +19 -0
- package/build/cli.d.ts +3 -3
- package/build/cli.js +21 -187
- package/build/command.d.ts +9 -0
- package/build/command.js +20 -0
- package/build/doctor/doctor.d.ts +1 -1
- package/build/doctor/doctor.js +64 -56
- package/build/doctor/index.d.ts +2 -0
- package/build/doctor/index.js +16 -0
- package/build/exportEmbed/export.d.ts +2 -2
- package/build/exportEmbed/export.js +53 -49
- package/build/exportEmbed/index.d.ts +2 -0
- package/build/exportEmbed/index.js +19 -0
- package/build/file.d.ts +2 -1
- package/build/file.js +18 -8
- package/build/help.d.ts +1 -0
- package/build/help.js +36 -0
- package/build/index.d.ts +5 -3
- package/build/index.js +6 -5
- package/build/internal/prebuild.d.ts +3 -0
- package/build/internal/prebuild.js +9 -0
- package/build/log.d.ts +4 -0
- package/build/log.js +6 -0
- package/build/modules/index.d.ts +2 -0
- package/build/modules/index.js +16 -0
- package/build/native/bare.d.ts +4 -0
- package/build/native/bare.js +130 -0
- package/build/native/build.d.ts +15 -0
- package/build/native/build.js +67 -0
- package/build/{run → native}/cache.d.ts +2 -2
- package/build/{run → native}/cache.js +8 -8
- package/build/native/hvigor.d.ts +2 -0
- package/build/native/hvigor.js +77 -0
- package/build/native/install.d.ts +5 -0
- package/build/native/install.js +13 -0
- package/build/native/prepareProject.d.ts +11 -0
- package/build/native/prepareProject.js +68 -0
- package/build/native/project.d.ts +8 -0
- package/build/native/project.js +58 -0
- package/build/native/toolchain.d.ts +16 -0
- package/build/{tools.js → native/toolchain.js} +1 -57
- package/build/native/types.d.ts +47 -0
- package/build/native/types.js +2 -0
- package/build/prebuild/check.d.ts +7 -3
- package/build/prebuild/check.js +26 -20
- package/build/prebuild/clean.js +5 -5
- package/build/prebuild/index.d.ts +2 -0
- package/build/prebuild/index.js +51 -0
- package/build/prebuild/prebuild.js +11 -3
- package/build/prebuild/template.js +2 -2
- package/build/process.d.ts +11 -1
- package/build/process.js +16 -0
- package/build/profile.d.ts +1 -0
- package/build/profile.js +12 -0
- package/build/projectLock.js +7 -2
- package/build/run/devices.d.ts +1 -1
- package/build/run/devices.js +15 -10
- package/build/run/emulators.d.ts +1 -1
- package/build/run/emulators.js +2 -5
- package/build/run/index.d.ts +2 -0
- package/build/run/index.js +25 -0
- package/build/run/metro.js +1 -1
- package/build/run/run.d.ts +4 -10
- package/build/run/run.js +45 -90
- package/build/start/index.d.ts +2 -0
- package/build/start/index.js +33 -0
- package/package.json +14 -4
- package/src/bin/expo-harmony.ts +2 -2
- package/src/buildHap/build.ts +13 -105
- package/src/buildHap/index.ts +19 -0
- package/src/cli.ts +24 -227
- package/src/command.ts +24 -0
- package/src/doctor/doctor.ts +63 -61
- package/src/doctor/index.ts +16 -0
- package/src/exportEmbed/export.ts +64 -55
- package/src/exportEmbed/index.ts +17 -0
- package/src/file.ts +20 -11
- package/src/help.ts +33 -0
- package/src/index.ts +5 -3
- package/src/internal/prebuild.ts +3 -0
- package/src/log.ts +5 -0
- package/src/modules/index.ts +16 -0
- package/src/native/bare.ts +145 -0
- package/src/native/build.ts +81 -0
- package/src/{run → native}/cache.ts +10 -9
- package/src/native/hvigor.ts +81 -0
- package/src/native/install.ts +17 -0
- package/src/native/prepareProject.ts +99 -0
- package/src/native/project.ts +88 -0
- package/src/{tools.ts → native/toolchain.ts} +11 -118
- package/src/native/types.ts +44 -0
- package/src/prebuild/check.ts +30 -19
- package/src/prebuild/clean.ts +3 -3
- package/src/prebuild/index.ts +56 -0
- package/src/prebuild/prebuild.ts +11 -1
- package/src/prebuild/template.ts +2 -2
- package/src/process.ts +38 -1
- package/src/profile.ts +13 -0
- package/src/projectLock.ts +9 -1
- package/src/run/devices.ts +16 -11
- package/src/run/emulators.ts +4 -6
- package/src/run/index.ts +25 -0
- package/src/run/metro.ts +1 -1
- package/src/run/run.ts +56 -129
- package/src/start/index.ts +31 -0
- package/build/buildHap/common.d.ts +0 -24
- package/build/buildHap/common.js +0 -92
- package/build/run/install.d.ts +0 -7
- package/build/run/install.js +0 -26
- package/build/tools.d.ts +0 -39
- package/src/buildHap/common.ts +0 -145
- package/src/run/install.ts +0 -41
package/src/cli.ts
CHANGED
|
@@ -1,245 +1,42 @@
|
|
|
1
|
-
import
|
|
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 {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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 {
|
|
30
|
+
const { command } = await commands[name]();
|
|
185
31
|
|
|
186
|
-
|
|
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
|
|
35
|
+
export async function mainAsync(argv: string[] = process.argv.slice(2)): Promise<void> {
|
|
232
36
|
try {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
process.exitCode = code;
|
|
37
|
+
process.exitCode = await runAsync(argv);
|
|
236
38
|
} catch (error) {
|
|
237
|
-
|
|
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
|
+
}
|
package/src/doctor/doctor.ts
CHANGED
|
@@ -5,17 +5,14 @@ import path from 'node:path';
|
|
|
5
5
|
import { getConfig } from '@expo/config';
|
|
6
6
|
import { normalizeHarmonyConfig } from '@expo-harmony/config-plugins';
|
|
7
7
|
import { verifyModulesAsync } from '@expo-harmony/expo-modules-autolinking';
|
|
8
|
-
import { isRnohAutolinkingDisabled } from '@expo-harmony/prebuild-config/
|
|
9
|
-
import { validateHarmonySigningConfigFile } from '@expo-harmony/prebuild-config/signing';
|
|
8
|
+
import { isRnohAutolinkingDisabled, validateHarmonySigningConfigFile } from '@expo-harmony/prebuild-config/internal';
|
|
10
9
|
|
|
11
10
|
import { spawnAsync } from '../process';
|
|
12
11
|
import { withHarmonyProjectLockAsync } from '../projectLock';
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
resolveHarmonyToolchain,
|
|
16
|
-
type HarmonyTool,
|
|
17
|
-
} from '../tools';
|
|
12
|
+
import { resolveHarmonyBuildPlanIfPresentAsync } from '../native/project';
|
|
13
|
+
import { resolveHarmonyToolchain, type HarmonyTool } from '../native/toolchain';
|
|
18
14
|
import { RequiredProjectPackages } from '../upstream';
|
|
15
|
+
import { isBareHarmonyProject } from '../native/bare';
|
|
19
16
|
|
|
20
17
|
export interface DoctorCheck {
|
|
21
18
|
details?: unknown;
|
|
@@ -37,22 +34,23 @@ interface DoctorOptions {
|
|
|
37
34
|
validateModules?: boolean;
|
|
38
35
|
}
|
|
39
36
|
|
|
40
|
-
function hasPlugin(plugins,
|
|
41
|
-
return (plugins || []).some(plugin => (Array.isArray(plugin) ? plugin[0] : plugin) ===
|
|
37
|
+
function hasPlugin(plugins, name) {
|
|
38
|
+
return (plugins || []).some(plugin => (Array.isArray(plugin) ? plugin[0] : plugin) === name);
|
|
42
39
|
}
|
|
43
40
|
|
|
44
41
|
function check(id: string, status: DoctorCheck['status'], message: string, details?: unknown): DoctorCheck {
|
|
45
42
|
return { id, status, message, ...(details ? { details } : {}) };
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
function canResolvePackage(
|
|
49
|
-
const
|
|
45
|
+
function canResolvePackage(root, name) {
|
|
46
|
+
const require = createRequire(path.join(root, 'package.json'));
|
|
47
|
+
|
|
50
48
|
try {
|
|
51
|
-
|
|
49
|
+
require.resolve(`${name}/package.json`);
|
|
52
50
|
return true;
|
|
53
51
|
} catch {
|
|
54
52
|
try {
|
|
55
|
-
|
|
53
|
+
require.resolve(name);
|
|
56
54
|
return true;
|
|
57
55
|
} catch {
|
|
58
56
|
return false;
|
|
@@ -60,20 +58,20 @@ function canResolvePackage(projectRoot, packageName) {
|
|
|
60
58
|
}
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
async function validateMetroConfigAsync(
|
|
64
|
-
const
|
|
65
|
-
let
|
|
61
|
+
async function validateMetroConfigAsync(root) {
|
|
62
|
+
const require = createRequire(path.join(root, 'package.json'));
|
|
63
|
+
let metro;
|
|
66
64
|
|
|
67
65
|
try {
|
|
68
|
-
|
|
66
|
+
metro = require('metro-config');
|
|
69
67
|
} catch (cause) {
|
|
70
68
|
throw new Error('Cannot load the project-local metro-config package.', { cause });
|
|
71
69
|
}
|
|
72
70
|
|
|
73
|
-
if (typeof
|
|
71
|
+
if (typeof metro.resolveConfig !== 'function') {
|
|
74
72
|
const version = (() => {
|
|
75
73
|
try {
|
|
76
|
-
return
|
|
74
|
+
return require('metro-config/package.json').version;
|
|
77
75
|
} catch {
|
|
78
76
|
return 'unknown';
|
|
79
77
|
}
|
|
@@ -82,15 +80,15 @@ async function validateMetroConfigAsync(projectRoot) {
|
|
|
82
80
|
throw new Error(`metro-config ${version} does not expose resolveConfig().`);
|
|
83
81
|
}
|
|
84
82
|
|
|
85
|
-
const
|
|
86
|
-
const
|
|
83
|
+
const present = Object.hasOwn(process.env, 'EXPO_METRO_TARGET');
|
|
84
|
+
const previous = process.env.EXPO_METRO_TARGET;
|
|
87
85
|
let resolved;
|
|
88
86
|
|
|
89
87
|
try {
|
|
90
88
|
process.env.EXPO_METRO_TARGET = 'harmony';
|
|
91
|
-
resolved = await
|
|
89
|
+
resolved = await metro.resolveConfig(undefined, root);
|
|
92
90
|
} finally {
|
|
93
|
-
if (
|
|
91
|
+
if (present) process.env.EXPO_METRO_TARGET = previous;
|
|
94
92
|
else delete process.env.EXPO_METRO_TARGET;
|
|
95
93
|
}
|
|
96
94
|
|
|
@@ -105,33 +103,40 @@ async function validateMetroConfigAsync(projectRoot) {
|
|
|
105
103
|
}
|
|
106
104
|
}
|
|
107
105
|
|
|
108
|
-
async function doctorUnlockedAsync(
|
|
106
|
+
async function doctorUnlockedAsync(root: string, options: DoctorOptions = {}): Promise<DoctorResult> {
|
|
109
107
|
const checks: DoctorCheck[] = [];
|
|
110
|
-
const
|
|
108
|
+
const unavailable = options.requireBuildTools ? 'error' : 'warn';
|
|
111
109
|
let config;
|
|
110
|
+
const bare = isBareHarmonyProject(root);
|
|
112
111
|
|
|
113
112
|
try {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
113
|
+
if (bare) {
|
|
114
|
+
const plan = await resolveHarmonyBuildPlanIfPresentAsync(root);
|
|
115
|
+
checks.push(check('native-config', 'pass', `Bare Harmony project: ${plan.bundleName} / ${plan.moduleName}. Native files own the configuration.`));
|
|
116
|
+
} else {
|
|
117
|
+
config = getConfig(root, {
|
|
118
|
+
isModdedConfig: true,
|
|
119
|
+
skipSDKVersionRequirement: true,
|
|
120
|
+
}).exp;
|
|
121
|
+
normalizeHarmonyConfig(config);
|
|
122
|
+
checks.push(check('app-config', 'pass', 'Harmony app config is valid.'));
|
|
123
|
+
}
|
|
120
124
|
} catch (error) {
|
|
121
|
-
checks.push(check('app-config', 'error', error.message, { code: error.code || 'ERR_HARMONY_CONFIG_INVALID' }));
|
|
125
|
+
checks.push(check(bare ? 'native-config' : 'app-config', 'error', error.message, { code: error.code || 'ERR_HARMONY_CONFIG_INVALID' }));
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
if (config) {
|
|
125
129
|
checks.push(hasPlugin(config.plugins, '@expo-harmony/prebuild-config')
|
|
126
130
|
? check('config-plugin', 'pass', '@expo-harmony/prebuild-config is registered.')
|
|
127
131
|
: check('config-plugin', 'error', 'Add @expo-harmony/prebuild-config to expo.plugins.'));
|
|
132
|
+
|
|
128
133
|
const harmony = (config as typeof config & {
|
|
129
134
|
harmony?: { signingConfigFile?: string };
|
|
130
135
|
}).harmony;
|
|
131
136
|
|
|
132
137
|
if (harmony?.signingConfigFile) {
|
|
133
138
|
try {
|
|
134
|
-
const signing = await validateHarmonySigningConfigFile(
|
|
139
|
+
const signing = await validateHarmonySigningConfigFile(root, harmony.signingConfigFile);
|
|
135
140
|
checks.push(check('signing', 'pass', `Harmony signing config ${signing.name} is valid.`));
|
|
136
141
|
} catch (error) {
|
|
137
142
|
checks.push(check('signing', 'error', error.message, { code: error.code || 'ERR_HARMONY_SIGNING_INVALID' }));
|
|
@@ -142,23 +147,23 @@ async function doctorUnlockedAsync(projectRoot: string, options: DoctorOptions =
|
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
try {
|
|
145
|
-
await validateMetroConfigAsync(
|
|
150
|
+
await validateMetroConfigAsync(root);
|
|
146
151
|
checks.push(check('metro', 'pass', 'The resolved Metro config enables Harmony.'));
|
|
147
152
|
} catch (error) {
|
|
148
153
|
checks.push(check('metro', 'error', `Cannot load a Harmony-enabled Metro config: ${error.message}`, { code: error.code || 'ERR_HARMONY_METRO_CONFIG' }));
|
|
149
154
|
}
|
|
150
155
|
|
|
151
|
-
for (const
|
|
152
|
-
if (canResolvePackage(
|
|
153
|
-
checks.push(check(`package:${
|
|
156
|
+
for (const name of RequiredProjectPackages) {
|
|
157
|
+
if (canResolvePackage(root, name)) {
|
|
158
|
+
checks.push(check(`package:${name}`, 'pass', `${name} is resolvable.`));
|
|
154
159
|
} else {
|
|
155
|
-
checks.push(check(`package:${
|
|
160
|
+
checks.push(check(`package:${name}`, 'error', `${name} is not resolvable from the app.`));
|
|
156
161
|
}
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
if (options.validateModules !== false) {
|
|
160
165
|
try {
|
|
161
|
-
const result = await verifyModulesAsync({ platform: 'harmony', projectRoot });
|
|
166
|
+
const result = await verifyModulesAsync({ platform: 'harmony', projectRoot: root });
|
|
162
167
|
const errors = result.diagnostics.filter(item => item.severity === 'error');
|
|
163
168
|
const warnings = result.diagnostics.filter(item => item.severity === 'warning');
|
|
164
169
|
|
|
@@ -190,54 +195,55 @@ async function doctorUnlockedAsync(projectRoot: string, options: DoctorOptions =
|
|
|
190
195
|
}
|
|
191
196
|
|
|
192
197
|
const toolchain = resolveHarmonyToolchain();
|
|
193
|
-
let
|
|
198
|
+
let sdk;
|
|
194
199
|
|
|
195
200
|
if (toolchain.sdkHome) {
|
|
196
|
-
|
|
201
|
+
sdk = check('harmony-sdk', 'pass', `Complete Harmony SDK root was resolved at ${toolchain.sdkHome}.`);
|
|
197
202
|
} else {
|
|
198
|
-
|
|
203
|
+
sdk = check('harmony-sdk', unavailable, 'No complete Harmony SDK root with HMS and OpenHarmony components was found; generation works but HAP build cannot be verified.');
|
|
199
204
|
}
|
|
200
205
|
|
|
201
|
-
checks.push(
|
|
206
|
+
checks.push(sdk);
|
|
202
207
|
|
|
203
208
|
const tools: Array<[string, HarmonyTool, string[], DoctorCheck['status']]> = [
|
|
204
|
-
['ohpm', toolchain.ohpm, ['--version'],
|
|
205
|
-
['hvigor-command', toolchain.hvigor, ['--version'],
|
|
209
|
+
['ohpm', toolchain.ohpm, ['--version'], unavailable],
|
|
210
|
+
['hvigor-command', toolchain.hvigor, ['--version'], unavailable],
|
|
206
211
|
];
|
|
207
212
|
if (options.requireDeviceTools !== false) {
|
|
208
|
-
tools.unshift(['hdc', toolchain.hdc, ['-v'],
|
|
213
|
+
tools.unshift(['hdc', toolchain.hdc, ['-v'], unavailable]);
|
|
209
214
|
}
|
|
210
|
-
|
|
215
|
+
|
|
216
|
+
for (const [id, tool, args, status] of tools) {
|
|
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, ...
|
|
220
|
+
const result = await spawnAsync(tool.command, [...tool.args, ...args], {
|
|
215
221
|
capture: true,
|
|
216
|
-
cwd:
|
|
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,
|
|
229
|
+
: check(id, status, `${command} is unavailable or unhealthy; HAP build cannot be verified.`));
|
|
224
230
|
} catch {
|
|
225
|
-
checks.push(check(id,
|
|
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(
|
|
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', '
|
|
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
|
|
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(
|
|
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
|
+
};
|