@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.
- package/README.md +87 -38
- package/build/args.d.ts +19 -0
- package/build/args.js +27 -0
- package/build/bin/expo-harmony.d.ts +2 -0
- package/build/bin/expo-harmony.js +1 -1
- package/build/buildHap/build.d.ts +18 -0
- package/build/buildHap/build.js +38 -0
- package/build/buildHap/index.d.ts +2 -0
- package/build/buildHap/index.js +19 -0
- package/build/buildHap/options.d.ts +7 -0
- package/build/buildHap/options.js +30 -0
- package/build/cli.d.ts +3 -0
- package/build/cli.js +40 -0
- package/build/command.d.ts +9 -0
- package/build/command.js +20 -0
- package/build/development/manifest.d.ts +1 -0
- package/build/development/manifest.js +121 -0
- package/build/development/protocol.d.ts +3 -0
- package/build/development/protocol.js +33 -0
- package/build/development/session.d.ts +5 -0
- package/build/development/session.js +66 -0
- package/build/doctor/doctor.d.ts +20 -0
- package/build/doctor/doctor.js +226 -0
- package/build/doctor/index.d.ts +2 -0
- package/build/doctor/index.js +16 -0
- package/build/doctor/options.d.ts +5 -0
- package/build/doctor/options.js +15 -0
- package/build/entry.d.ts +2 -0
- package/build/entry.js +47 -0
- package/build/errors.d.ts +12 -0
- package/build/errors.js +16 -0
- package/build/expo.d.ts +19 -0
- package/build/expo.js +53 -0
- package/build/exportEmbed/export.d.ts +18 -0
- package/build/exportEmbed/export.js +124 -0
- package/build/exportEmbed/index.d.ts +2 -0
- package/build/exportEmbed/index.js +19 -0
- package/build/exportEmbed/manifest.d.ts +44 -0
- package/build/exportEmbed/manifest.js +171 -0
- package/build/exportEmbed/options.d.ts +7 -0
- package/build/exportEmbed/options.js +22 -0
- package/build/file.d.ts +10 -0
- package/build/file.js +110 -0
- package/build/help.d.ts +1 -0
- package/build/help.js +44 -0
- package/build/index.d.ts +14 -0
- package/build/index.js +6 -5
- package/build/internal/development.d.ts +1 -0
- package/build/internal/development.js +8 -0
- package/build/internal/export.d.ts +1 -0
- package/build/internal/export.js +80 -0
- 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/modules/modules.d.ts +42 -0
- package/build/modules/modules.js +84 -0
- package/build/modules/options.d.ts +10 -0
- package/build/modules/options.js +45 -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 +69 -0
- package/build/native/cache.d.ts +11 -0
- package/build/native/cache.js +73 -0
- package/build/native/hvigor.d.ts +3 -0
- package/build/native/hvigor.js +83 -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/native/toolchain.js +172 -0
- package/build/native/types.d.ts +47 -0
- package/build/native/types.js +2 -0
- package/build/path.d.ts +4 -0
- package/build/path.js +27 -0
- package/build/prebuild/check.d.ts +10 -0
- package/build/prebuild/check.js +151 -0
- package/build/prebuild/clean.d.ts +2 -0
- package/build/prebuild/clean.js +50 -0
- package/build/prebuild/index.d.ts +2 -0
- package/build/prebuild/index.js +51 -0
- package/build/prebuild/options.d.ts +10 -0
- package/build/prebuild/options.js +50 -0
- package/build/prebuild/prebuild.d.ts +6 -0
- package/build/prebuild/prebuild.js +70 -0
- package/build/prebuild/template.d.ts +8 -0
- package/build/prebuild/template.js +86 -0
- package/build/process.d.ts +41 -0
- package/build/process.js +324 -0
- package/build/profile.d.ts +1 -0
- package/build/profile.js +12 -0
- package/build/project.d.ts +2 -0
- package/build/project.js +23 -0
- package/build/projectLock.d.ts +2 -0
- package/build/projectLock.js +185 -0
- package/build/run/devices.d.ts +29 -0
- package/build/run/devices.js +223 -0
- package/build/run/emulators.d.ts +13 -0
- package/build/run/emulators.js +82 -0
- package/build/run/index.d.ts +2 -0
- package/build/run/index.js +25 -0
- package/build/run/metro.d.ts +21 -0
- package/build/run/metro.js +162 -0
- package/build/run/options.d.ts +14 -0
- package/build/run/options.js +62 -0
- package/build/run/run.d.ts +46 -0
- package/build/run/run.js +135 -0
- package/build/runtime/config.d.ts +37 -0
- package/build/runtime/config.js +169 -0
- package/build/runtime/contract.d.ts +5 -0
- package/build/runtime/contract.js +109 -0
- package/build/runtime/fingerprint.d.ts +1 -0
- package/build/runtime/fingerprint.js +49 -0
- package/build/runtime/index.d.ts +2 -0
- package/build/runtime/index.js +50 -0
- package/build/start/index.d.ts +2 -0
- package/build/start/index.js +38 -0
- package/build/start/options.d.ts +9 -0
- package/build/start/options.js +38 -0
- package/build/updates/export.d.ts +14 -0
- package/build/updates/export.js +46 -0
- package/build/updates/index.d.ts +2 -0
- package/build/updates/index.js +96 -0
- package/build/upstream.d.ts +9 -0
- package/build/upstream.js +17 -0
- package/package.json +21 -5
- 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 +27 -225
- package/src/command.ts +24 -0
- package/src/development/manifest.ts +127 -0
- package/src/development/protocol.ts +42 -0
- package/src/development/session.ts +83 -0
- package/src/doctor/doctor.ts +67 -62
- package/src/doctor/index.ts +16 -0
- package/src/exportEmbed/export.ts +73 -72
- package/src/exportEmbed/index.ts +17 -0
- package/src/file.ts +20 -11
- package/src/help.ts +41 -0
- package/src/index.ts +5 -3
- package/src/internal/development.ts +8 -0
- package/src/internal/export.ts +82 -0
- 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 +83 -0
- package/src/{run → native}/cache.ts +10 -9
- package/src/native/hvigor.ts +88 -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} +38 -140
- package/src/native/types.ts +44 -0
- package/src/prebuild/check.ts +56 -31
- 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 +175 -64
- package/src/profile.ts +13 -0
- package/src/projectLock.ts +9 -1
- package/src/run/devices.ts +24 -11
- package/src/run/emulators.ts +4 -6
- package/src/run/index.ts +25 -0
- package/src/run/metro.ts +10 -1
- package/src/run/options.ts +2 -0
- package/src/run/run.ts +63 -129
- package/src/runtime/config.ts +207 -0
- package/src/runtime/contract.ts +167 -0
- package/src/runtime/fingerprint.ts +49 -0
- package/src/runtime/index.ts +54 -0
- package/src/start/index.ts +37 -0
- package/src/start/options.ts +12 -0
- package/src/updates/export.ts +60 -0
- package/src/updates/index.ts +89 -0
- package/src/buildHap/common.ts +0 -145
- package/src/run/install.ts +0 -41
package/src/cli.ts
CHANGED
|
@@ -1,242 +1,44 @@
|
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
`;
|
|
3
|
+
import { Help } from './help';
|
|
4
|
+
import type { Log } from './log';
|
|
5
|
+
|
|
6
|
+
const commands: Record<string, () => Promise<{ command: Command }>> = {
|
|
7
|
+
'runtime': () => import('./runtime/index.js'),
|
|
8
|
+
'start': () => import('./start/index.js'),
|
|
9
|
+
'prebuild': () => import('./prebuild/index.js'),
|
|
10
|
+
'build': () => import('./buildHap/index.js'),
|
|
11
|
+
'doctor': () => import('./doctor/index.js'),
|
|
12
|
+
'modules': () => import('./modules/index.js'),
|
|
13
|
+
'export': () => import('./updates/index.js'),
|
|
14
|
+
'export:embed': () => import('./exportEmbed/index.js'),
|
|
15
|
+
'run': () => import('./run/index.js'),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export async function runAsync(argv: string[] = process.argv.slice(2), io: Log = console): Promise<number> {
|
|
19
|
+
process.env.EXPO_HARMONY = '1';
|
|
20
|
+
process.env.EXPO_METRO_TARGET = 'harmony';
|
|
47
21
|
|
|
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
22
|
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> {
|
|
93
|
-
const invocation = parseInvocation(argv);
|
|
94
|
-
if (invocation.command === 'help') {
|
|
95
23
|
io.log(Help);
|
|
96
24
|
return 0;
|
|
97
25
|
}
|
|
98
26
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
...invocation.parsed,
|
|
103
|
-
interactive: true,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
if (metro.owner === 'existing') {
|
|
108
|
-
io.log(`Expo Metro is already running on port ${metro.port}.`);
|
|
109
|
-
if (invocation.parsed.resetCache) {
|
|
110
|
-
io.warn('Stop the existing Metro server and run this command again to reset its cache.');
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
io.log('\n› Logs for your project will appear below. Press Ctrl+C to exit.');
|
|
114
|
-
await metro.waitAsync();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return 0;
|
|
118
|
-
} finally {
|
|
119
|
-
await metro.stop();
|
|
120
|
-
}
|
|
27
|
+
const name = argv[0];
|
|
28
|
+
if (!Object.hasOwn(commands, name)) {
|
|
29
|
+
throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', `Unknown command: ${name}`, { operation: 'parse-arguments' });
|
|
121
30
|
}
|
|
122
31
|
|
|
123
|
-
|
|
124
|
-
const { doctorAsync, formatDoctor } = await import('./doctor/doctor.js');
|
|
125
|
-
const result = await doctorAsync(invocation.projectRoot, { requireBuildTools: true });
|
|
126
|
-
|
|
127
|
-
io.log(formatDoctor(result));
|
|
128
|
-
|
|
129
|
-
return result.ok ? 0 : 1;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (invocation.command === 'build') {
|
|
133
|
-
const { buildHarmonyAsync } = await import('./buildHap/build.js');
|
|
134
|
-
const result = await buildHarmonyAsync(invocation.projectRoot, {
|
|
135
|
-
...invocation.parsed,
|
|
136
|
-
io,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
io.log(`Built ${result.variant} HAP at ${result.hapPath}; no device actions were performed.`);
|
|
32
|
+
const { command } = await commands[name]();
|
|
140
33
|
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (invocation.command === 'export:embed') {
|
|
145
|
-
const { exportEmbedAsync } = await import('./exportEmbed/export.js');
|
|
146
|
-
const result = await exportEmbedAsync(invocation.projectRoot, invocation.parsed);
|
|
147
|
-
|
|
148
|
-
if (invocation.parsed.check) io.log('Harmony export bundle, assets, and source map are valid.');
|
|
149
|
-
else io.log(`Exported Hermes bytecode ${result.bundle.path} with ${result.assets.length} asset file(s).`);
|
|
150
|
-
|
|
151
|
-
return 0;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (invocation.command === 'modules') {
|
|
155
|
-
const { formatModulesResult, runModulesCommandAsync } = await import('./modules/modules.js');
|
|
156
|
-
const result = await runModulesCommandAsync(invocation.projectRoot, invocation.parsed);
|
|
157
|
-
|
|
158
|
-
io.log(formatModulesResult(result));
|
|
159
|
-
return result.ok ? 0 : 1;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (invocation.command === 'run') {
|
|
163
|
-
const { runHarmonySessionAsync } = await import('./run/run.js');
|
|
164
|
-
|
|
165
|
-
const session = await runHarmonySessionAsync(invocation.projectRoot, {
|
|
166
|
-
...invocation.parsed,
|
|
167
|
-
interactiveBundler: true,
|
|
168
|
-
io,
|
|
169
|
-
});
|
|
170
|
-
const { result } = session;
|
|
171
|
-
|
|
172
|
-
io.log(`Launched ${result.bundleName} on ${result.device.id} (${result.variant}).`);
|
|
173
|
-
if (session.metro.owner === 'started') {
|
|
174
|
-
io.log('\n› Logs for your project will appear below. Press Ctrl+C to exit.');
|
|
175
|
-
await session.metro.waitAsync();
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return 0;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const { check, clean, passthrough } = invocation.parsed;
|
|
182
|
-
|
|
183
|
-
if (check && passthrough.length) {
|
|
184
|
-
throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--check cannot be combined with mutating prebuild options.');
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return withHarmonyProjectLockAsync(
|
|
188
|
-
invocation.projectRoot,
|
|
189
|
-
check ? 'prebuild-check' : 'prebuild',
|
|
190
|
-
async () => {
|
|
191
|
-
if (clean) {
|
|
192
|
-
const { assertSafeCleanTarget } = await import('./prebuild/clean.js');
|
|
193
|
-
await assertSafeCleanTarget(invocation.projectRoot);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const { doctorAsync, formatDoctor } = await import('./doctor/doctor.js');
|
|
197
|
-
const doctor = await doctorAsync(invocation.projectRoot, {
|
|
198
|
-
requireBuildTools: false,
|
|
199
|
-
validateGeneratedProject: !clean,
|
|
200
|
-
validateModules: false,
|
|
201
|
-
});
|
|
202
|
-
if (!doctor.ok) {
|
|
203
|
-
io.error(formatDoctor(doctor));
|
|
204
|
-
throw new HarmonyCliError('ERR_HARMONY_DOCTOR_FAILED', 'Harmony doctor found blocking errors.', {
|
|
205
|
-
operation: 'doctor',
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
for (const item of doctor.checks.filter(item => item.status === 'warn')) io.warn(`! ${item.message}`);
|
|
210
|
-
|
|
211
|
-
if (check) {
|
|
212
|
-
const { checkAsync } = await import('./prebuild/check.js');
|
|
213
|
-
const result = await checkAsync(invocation.projectRoot);
|
|
214
|
-
|
|
215
|
-
if (result.clean) io.log('Harmony CNG output is up to date.');
|
|
216
|
-
else for (const change of result.changes) io.log(`${change.type}: ${change.path}`);
|
|
217
|
-
|
|
218
|
-
return result.clean ? 0 : 2;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const { prebuildParsedAsync } = await import('./prebuild/prebuild.js');
|
|
222
|
-
await prebuildParsedAsync(invocation.projectRoot, passthrough);
|
|
223
|
-
|
|
224
|
-
return 0;
|
|
225
|
-
});
|
|
34
|
+
return command(argv.slice(1), io);
|
|
226
35
|
}
|
|
227
36
|
|
|
228
|
-
async function
|
|
37
|
+
export async function mainAsync(argv: string[] = process.argv.slice(2)): Promise<void> {
|
|
229
38
|
try {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
process.exitCode = code;
|
|
39
|
+
process.exitCode = await runAsync(argv);
|
|
233
40
|
} catch (error) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
console.error(`[${code}] ${error.message}`);
|
|
237
|
-
|
|
41
|
+
console.error(`[${error.code || 'ERR_HARMONY_UNKNOWN'}] ${error.message}`);
|
|
238
42
|
process.exitCode = error.exitCode || 1;
|
|
239
43
|
}
|
|
240
44
|
}
|
|
241
|
-
|
|
242
|
-
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
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
|
|
4
|
+
import { HarmonyCliError } from '../errors';
|
|
5
|
+
import { resolveExpoCli } from '../expo';
|
|
6
|
+
import { resolveHarmonyBuildPlanAsync } from '../native/project';
|
|
7
|
+
import { resolveRuntimeRequirementsAsync } from '../runtime/contract';
|
|
8
|
+
import { canonicalHarmonyManifestURL, createHarmonyLaunchLink, HarmonyManifestPath } from './protocol';
|
|
9
|
+
|
|
10
|
+
async function createManifestResponseAsync(middleware, options, require: NodeRequire) {
|
|
11
|
+
const { ExpoGoManifestHandlerMiddleware: Manifest, ResponseContentType: Types } = require('./build/src/start/server/middleware/ExpoGoManifestHandlerMiddleware');
|
|
12
|
+
const { getCodeSigningInfoAsync, signManifestString } = require('./build/src/utils/codesigning');
|
|
13
|
+
const { serializeDictionary } = require('structured-headers');
|
|
14
|
+
const { exp, expoGoConfig, bundleUrl, hostUri } = await middleware._resolveProjectSettingsAsync(options);
|
|
15
|
+
const requirements = await resolveRuntimeRequirementsAsync(middleware.projectRoot);
|
|
16
|
+
const signing = await getCodeSigningInfoAsync(exp, options.expectSignature, middleware.options.privateKeyPath);
|
|
17
|
+
const scope = await Manifest.getScopeKeyAsync({ slug: exp.slug, codeSigningInfo: signing });
|
|
18
|
+
|
|
19
|
+
const bundle = new URL(bundleUrl);
|
|
20
|
+
bundle.searchParams.set('transform.engine', 'hermes');
|
|
21
|
+
bundle.searchParams.set('transform.bytecode', '0');
|
|
22
|
+
bundle.searchParams.set('unstable_transformProfile', 'hermes-stable');
|
|
23
|
+
bundle.searchParams.set('lazy', 'false');
|
|
24
|
+
|
|
25
|
+
const name = exp.harmony?.bundleName || (await resolveHarmonyBuildPlanAsync(middleware.projectRoot)).bundleName;
|
|
26
|
+
const id = exp.extra?.eas?.projectId;
|
|
27
|
+
const address = canonicalHarmonyManifestURL(`${bundle.origin}${options.harmonyManifestPath ?? `${HarmonyManifestPath}?platform=harmony`}`);
|
|
28
|
+
const manifest = JSON.stringify({
|
|
29
|
+
id: crypto.randomUUID(),
|
|
30
|
+
createdAt: new Date().toISOString(),
|
|
31
|
+
runtimeVersion: requirements.runtimeVersion,
|
|
32
|
+
launchAsset: { key: 'bundle', contentType: 'application/javascript', url: bundle.toString() },
|
|
33
|
+
assets: [],
|
|
34
|
+
metadata: {},
|
|
35
|
+
extra: {
|
|
36
|
+
eas: { projectId: id ?? undefined },
|
|
37
|
+
expoClient: { ...exp, hostUri },
|
|
38
|
+
expoGo: expoGoConfig,
|
|
39
|
+
scopeKey: scope,
|
|
40
|
+
harmony: {
|
|
41
|
+
projectId: id || `@${exp.owner || 'anonymous'}/${exp.slug}/${name}`,
|
|
42
|
+
manifestUrl: address,
|
|
43
|
+
launchLink: createHarmonyLaunchLink(address),
|
|
44
|
+
requirements,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
const signature = signing
|
|
49
|
+
? serializeDictionary(new Map(Object.entries({
|
|
50
|
+
keyid: signing.keyId,
|
|
51
|
+
sig: signManifestString(manifest, signing),
|
|
52
|
+
alg: 'rsa-v1_5-sha256',
|
|
53
|
+
}).map(([key, value]) => [key, [value, new Map()]])))
|
|
54
|
+
: undefined;
|
|
55
|
+
|
|
56
|
+
if (options.responseContentType === Types.MULTIPART_MIXED) {
|
|
57
|
+
return middleware.encodeFormDataAsync({
|
|
58
|
+
stringifiedManifest: manifest,
|
|
59
|
+
manifestPartHeaders: signature ? { 'expo-signature': signature } : undefined,
|
|
60
|
+
certificateChainBody: signing?.certificateChainForResponse.join('\n') ?? null,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const headers = middleware.getDefaultResponseHeaders();
|
|
65
|
+
headers.set('content-type', Manifest.getContentTypeForResponseContentType(options.responseContentType));
|
|
66
|
+
if (signature) headers.set('expo-signature', signature);
|
|
67
|
+
|
|
68
|
+
return new Response(manifest, { headers });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function installHarmonyManifest(root: string) {
|
|
72
|
+
const expo = createRequire(resolveExpoCli(root).cliPath);
|
|
73
|
+
const require = createRequire(expo.resolve('@expo/cli/package.json'));
|
|
74
|
+
const version = require('./package.json').version;
|
|
75
|
+
if (version !== '55.0.32') {
|
|
76
|
+
throw new HarmonyCliError('ERR_HARMONY_EXPO_CLI_UNSUPPORTED', `The Harmony manifest adapter requires @expo/cli 55.0.32; found ${version}.`, { operation: 'development-manifest' });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { BundlerDevServer } = require('./build/src/start/server/BundlerDevServer');
|
|
80
|
+
const { parsePlatformHeader } = require('./build/src/start/server/middleware/resolvePlatform');
|
|
81
|
+
const create = BundlerDevServer.prototype.getManifestMiddlewareAsync;
|
|
82
|
+
|
|
83
|
+
// Reuse Expo's terminal QR renderer and dev-session publishing with our
|
|
84
|
+
// registered link, rather than emitting a second, incompatible QR code.
|
|
85
|
+
const nativeURL = BundlerDevServer.prototype.getNativeRuntimeUrl;
|
|
86
|
+
const redirectURL = BundlerDevServer.prototype.getRedirectUrl;
|
|
87
|
+
BundlerDevServer.prototype.getNativeRuntimeUrl = function (options = {}) {
|
|
88
|
+
const address = this.getUrlCreator().constructUrl({ ...options, scheme: 'http' });
|
|
89
|
+
if (!address) return nativeURL.call(this, options);
|
|
90
|
+
const manifest = new URL(HarmonyManifestPath, address);
|
|
91
|
+
manifest.searchParams.set('platform', 'harmony');
|
|
92
|
+
return createHarmonyLaunchLink(manifest.toString());
|
|
93
|
+
};
|
|
94
|
+
BundlerDevServer.prototype.getRedirectUrl = function (platform = null) {
|
|
95
|
+
return platform === null || platform === 'harmony'
|
|
96
|
+
? this.getNativeRuntimeUrl()
|
|
97
|
+
: redirectURL.call(this, platform);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Expo installs the manifest before Metro's enhancer; adapt only the created instance.
|
|
101
|
+
BundlerDevServer.prototype.getManifestMiddlewareAsync = async function (this: object, ...args) {
|
|
102
|
+
const middleware = await create.apply(this, args);
|
|
103
|
+
const parse = middleware.getParsedHeaders;
|
|
104
|
+
const respond = middleware._getManifestResponseAsync;
|
|
105
|
+
|
|
106
|
+
middleware.getParsedHeaders = (request) => {
|
|
107
|
+
if (parsePlatformHeader(request) !== 'harmony') return parse.call(middleware, request);
|
|
108
|
+
|
|
109
|
+
// Bypass only the upstream platform assertion without changing the actual request.
|
|
110
|
+
const url = new URL(request.url, 'http://localhost');
|
|
111
|
+
url.searchParams.delete('platform');
|
|
112
|
+
url.searchParams.append('platform', 'harmony');
|
|
113
|
+
const manifest = new URL(canonicalHarmonyManifestURL(url.toString()));
|
|
114
|
+
const harmonyManifestPath = manifest.pathname + manifest.search;
|
|
115
|
+
url.searchParams.set('platform', 'ios');
|
|
116
|
+
const copy = Object.create(request);
|
|
117
|
+
copy.url = url.pathname + url.search;
|
|
118
|
+
|
|
119
|
+
return { ...parse.call(middleware, copy), platform: 'harmony', harmonyManifestPath };
|
|
120
|
+
};
|
|
121
|
+
middleware._getManifestResponseAsync = options => options.platform === 'harmony'
|
|
122
|
+
? createManifestResponseAsync(middleware, options, require)
|
|
123
|
+
: respond.call(middleware, options);
|
|
124
|
+
|
|
125
|
+
return middleware;
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { HarmonyCliError } from '../errors';
|
|
2
|
+
|
|
3
|
+
export const HarmonyManifestPath = '/manifest';
|
|
4
|
+
|
|
5
|
+
export function canonicalHarmonyManifestURL(value: string): string {
|
|
6
|
+
let url: URL;
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
url = new URL(value);
|
|
10
|
+
decodeURIComponent(url.search);
|
|
11
|
+
} catch (cause) {
|
|
12
|
+
throw new HarmonyCliError(
|
|
13
|
+
'ERR_HARMONY_MANIFEST_URL',
|
|
14
|
+
'Expected an absolute HTTP(S) Harmony manifest URL.',
|
|
15
|
+
{ cause, operation: 'development-manifest' }
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash
|
|
20
|
+
|| !['/', HarmonyManifestPath, '/index.exp'].includes(url.pathname)
|
|
21
|
+
|| url.searchParams.getAll('platform').length !== 1 || url.searchParams.get('platform') !== 'harmony') {
|
|
22
|
+
throw new HarmonyCliError(
|
|
23
|
+
'ERR_HARMONY_MANIFEST_URL',
|
|
24
|
+
'Expected an HTTP(S) Expo manifest URL with platform=harmony and without credentials or a fragment.',
|
|
25
|
+
{ operation: 'development-manifest' }
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
url.pathname = HarmonyManifestPath;
|
|
30
|
+
// Keep all other query values (including repetitions) in order. Platform has
|
|
31
|
+
// one canonical position, and URLSearchParams normalizes equivalent encoding.
|
|
32
|
+
url.searchParams.delete('platform');
|
|
33
|
+
url.searchParams.append('platform', 'harmony');
|
|
34
|
+
return url.toString();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createHarmonyLaunchLink(manifest: string): string {
|
|
38
|
+
const url = new URL('expo-harmony://open');
|
|
39
|
+
url.searchParams.set('url', canonicalHarmonyManifestURL(manifest));
|
|
40
|
+
|
|
41
|
+
return url.toString();
|
|
42
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
|
|
4
|
+
import type { HarmonyDevelopmentManifest } from '@expo-harmony/expo-modules-autolinking/runtime';
|
|
5
|
+
|
|
6
|
+
import { HarmonyCliError } from '../errors';
|
|
7
|
+
import { readProjectRuntimeAsync } from '../runtime/config';
|
|
8
|
+
import { createHarmonyLaunchLink, HarmonyManifestPath } from './protocol';
|
|
9
|
+
|
|
10
|
+
export async function readDevelopmentSessionAsync(root: string, port: number, hostname?: string) {
|
|
11
|
+
const project = await readProjectRuntimeAsync(root);
|
|
12
|
+
const { config } = project;
|
|
13
|
+
|
|
14
|
+
const host = hostname || process.env.REACT_NATIVE_PACKAGER_HOSTNAME
|
|
15
|
+
|| Object.values(os.networkInterfaces()).flat().find(item => item?.family === 'IPv4' && !item.internal)?.address
|
|
16
|
+
|| '127.0.0.1';
|
|
17
|
+
if (!/^[A-Za-z0-9.-]+$/.test(host)) {
|
|
18
|
+
throw new HarmonyCliError(
|
|
19
|
+
'ERR_HARMONY_MANIFEST_HOST',
|
|
20
|
+
'Harmony development requires an HTTP LAN hostname or IPv4 address.',
|
|
21
|
+
{ operation: 'development-manifest' }
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const manifest = await new Promise<HarmonyDevelopmentManifest>((resolve, reject) => {
|
|
26
|
+
const request = http.get({
|
|
27
|
+
host: '127.0.0.1',
|
|
28
|
+
port,
|
|
29
|
+
path: `${HarmonyManifestPath}?platform=harmony`,
|
|
30
|
+
headers: { 'host': `${host}:${port}`, 'accept': 'application/json', 'expo-platform': 'harmony', 'expo-protocol-version': '0' },
|
|
31
|
+
}, (response) => {
|
|
32
|
+
let body = '';
|
|
33
|
+
|
|
34
|
+
response.setEncoding('utf8');
|
|
35
|
+
response.on('data', (chunk) => {
|
|
36
|
+
body += chunk;
|
|
37
|
+
|
|
38
|
+
if (body.length > 2 * 1024 * 1024) request.destroy(new Error('Harmony manifest exceeded 2 MB.'));
|
|
39
|
+
});
|
|
40
|
+
response.on('error', reject);
|
|
41
|
+
response.on('end', () => {
|
|
42
|
+
try {
|
|
43
|
+
const value = JSON.parse(body);
|
|
44
|
+
if (response.statusCode !== 200 || response.headers['expo-protocol-version'] !== '0'
|
|
45
|
+
|| !response.headers['content-type']?.startsWith('application/json')) {
|
|
46
|
+
reject(new Error(value.error?.message || 'Metro did not return an Expo development manifest with protocol version 0.'));
|
|
47
|
+
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
resolve(value);
|
|
52
|
+
} catch (cause) {
|
|
53
|
+
reject(new Error('Metro did not return a valid Harmony manifest.', { cause }));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
request.setTimeout(60_000, () => request.destroy(new Error('Harmony manifest request timed out.')));
|
|
59
|
+
request.on('error', reject);
|
|
60
|
+
}).catch((cause) => {
|
|
61
|
+
throw new HarmonyCliError(
|
|
62
|
+
'ERR_HARMONY_METRO_MANIFEST',
|
|
63
|
+
`Cannot use this Metro server: ${cause.message}`,
|
|
64
|
+
{ cause, operation: 'development-manifest' }
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const identity = config.extra?.eas?.projectId || `@${config.owner || 'anonymous'}/${config.slug}/${project.bundleName}`;
|
|
69
|
+
if (manifest.extra?.harmony?.projectId !== identity
|
|
70
|
+
|| manifest.runtimeVersion !== project.runtimeVersion) {
|
|
71
|
+
throw new HarmonyCliError(
|
|
72
|
+
'ERR_HARMONY_METRO_PROJECT',
|
|
73
|
+
'This Metro server belongs to another Harmony project.',
|
|
74
|
+
{ operation: 'development-manifest' }
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
manifestUrl: manifest.extra.harmony.manifestUrl,
|
|
80
|
+
launchLink: createHarmonyLaunchLink(manifest.extra.harmony.manifestUrl),
|
|
81
|
+
bundleUrl: manifest.launchAsset.url,
|
|
82
|
+
};
|
|
83
|
+
}
|