@expo-harmony/cli 55.0.26-harmony.1 → 55.0.26-harmony.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +87 -38
  2. package/build/args.d.ts +19 -0
  3. package/build/args.js +27 -0
  4. package/build/bin/expo-harmony.d.ts +2 -0
  5. package/build/bin/expo-harmony.js +1 -1
  6. package/build/buildHap/build.d.ts +18 -0
  7. package/build/buildHap/build.js +38 -0
  8. package/build/buildHap/index.d.ts +2 -0
  9. package/build/buildHap/index.js +19 -0
  10. package/build/buildHap/options.d.ts +7 -0
  11. package/build/buildHap/options.js +30 -0
  12. package/build/cli.d.ts +3 -0
  13. package/build/cli.js +40 -0
  14. package/build/command.d.ts +9 -0
  15. package/build/command.js +20 -0
  16. package/build/development/manifest.d.ts +1 -0
  17. package/build/development/manifest.js +100 -0
  18. package/build/development/protocol.d.ts +2 -0
  19. package/build/development/protocol.js +26 -0
  20. package/build/development/session.d.ts +5 -0
  21. package/build/development/session.js +66 -0
  22. package/build/doctor/doctor.d.ts +20 -0
  23. package/build/doctor/doctor.js +226 -0
  24. package/build/doctor/index.d.ts +2 -0
  25. package/build/doctor/index.js +16 -0
  26. package/build/doctor/options.d.ts +5 -0
  27. package/build/doctor/options.js +15 -0
  28. package/build/entry.d.ts +2 -0
  29. package/build/entry.js +47 -0
  30. package/build/errors.d.ts +12 -0
  31. package/build/errors.js +16 -0
  32. package/build/expo.d.ts +19 -0
  33. package/build/expo.js +53 -0
  34. package/build/exportEmbed/export.d.ts +18 -0
  35. package/build/exportEmbed/export.js +124 -0
  36. package/build/exportEmbed/index.d.ts +2 -0
  37. package/build/exportEmbed/index.js +19 -0
  38. package/build/exportEmbed/manifest.d.ts +44 -0
  39. package/build/exportEmbed/manifest.js +171 -0
  40. package/build/exportEmbed/options.d.ts +7 -0
  41. package/build/exportEmbed/options.js +22 -0
  42. package/build/file.d.ts +10 -0
  43. package/build/file.js +110 -0
  44. package/build/help.d.ts +1 -0
  45. package/build/help.js +44 -0
  46. package/build/index.d.ts +14 -0
  47. package/build/index.js +6 -5
  48. package/build/internal/development.d.ts +1 -0
  49. package/build/internal/development.js +8 -0
  50. package/build/internal/export.d.ts +1 -0
  51. package/build/internal/export.js +80 -0
  52. package/build/internal/prebuild.d.ts +3 -0
  53. package/build/internal/prebuild.js +9 -0
  54. package/build/log.d.ts +4 -0
  55. package/build/log.js +6 -0
  56. package/build/modules/index.d.ts +2 -0
  57. package/build/modules/index.js +16 -0
  58. package/build/modules/modules.d.ts +42 -0
  59. package/build/modules/modules.js +84 -0
  60. package/build/modules/options.d.ts +10 -0
  61. package/build/modules/options.js +45 -0
  62. package/build/native/bare.d.ts +4 -0
  63. package/build/native/bare.js +130 -0
  64. package/build/native/build.d.ts +15 -0
  65. package/build/native/build.js +69 -0
  66. package/build/native/cache.d.ts +11 -0
  67. package/build/native/cache.js +73 -0
  68. package/build/native/hvigor.d.ts +3 -0
  69. package/build/native/hvigor.js +83 -0
  70. package/build/native/install.d.ts +5 -0
  71. package/build/native/install.js +13 -0
  72. package/build/native/prepareProject.d.ts +11 -0
  73. package/build/native/prepareProject.js +68 -0
  74. package/build/native/project.d.ts +8 -0
  75. package/build/native/project.js +58 -0
  76. package/build/native/toolchain.d.ts +16 -0
  77. package/build/native/toolchain.js +172 -0
  78. package/build/native/types.d.ts +47 -0
  79. package/build/native/types.js +2 -0
  80. package/build/path.d.ts +4 -0
  81. package/build/path.js +27 -0
  82. package/build/prebuild/check.d.ts +10 -0
  83. package/build/prebuild/check.js +151 -0
  84. package/build/prebuild/clean.d.ts +2 -0
  85. package/build/prebuild/clean.js +50 -0
  86. package/build/prebuild/index.d.ts +2 -0
  87. package/build/prebuild/index.js +51 -0
  88. package/build/prebuild/options.d.ts +10 -0
  89. package/build/prebuild/options.js +50 -0
  90. package/build/prebuild/prebuild.d.ts +6 -0
  91. package/build/prebuild/prebuild.js +70 -0
  92. package/build/prebuild/template.d.ts +8 -0
  93. package/build/prebuild/template.js +86 -0
  94. package/build/process.d.ts +41 -0
  95. package/build/process.js +324 -0
  96. package/build/profile.d.ts +1 -0
  97. package/build/profile.js +12 -0
  98. package/build/project.d.ts +2 -0
  99. package/build/project.js +23 -0
  100. package/build/projectLock.d.ts +2 -0
  101. package/build/projectLock.js +185 -0
  102. package/build/run/devices.d.ts +29 -0
  103. package/build/run/devices.js +216 -0
  104. package/build/run/emulators.d.ts +13 -0
  105. package/build/run/emulators.js +82 -0
  106. package/build/run/index.d.ts +2 -0
  107. package/build/run/index.js +25 -0
  108. package/build/run/metro.d.ts +21 -0
  109. package/build/run/metro.js +162 -0
  110. package/build/run/options.d.ts +14 -0
  111. package/build/run/options.js +62 -0
  112. package/build/run/run.d.ts +46 -0
  113. package/build/run/run.js +135 -0
  114. package/build/runtime/config.d.ts +35 -0
  115. package/build/runtime/config.js +118 -0
  116. package/build/runtime/contract.d.ts +5 -0
  117. package/build/runtime/contract.js +108 -0
  118. package/build/runtime/fingerprint.d.ts +1 -0
  119. package/build/runtime/fingerprint.js +44 -0
  120. package/build/runtime/index.d.ts +2 -0
  121. package/build/runtime/index.js +50 -0
  122. package/build/start/index.d.ts +2 -0
  123. package/build/start/index.js +38 -0
  124. package/build/start/options.d.ts +9 -0
  125. package/build/start/options.js +38 -0
  126. package/build/updates/export.d.ts +14 -0
  127. package/build/updates/export.js +46 -0
  128. package/build/updates/index.d.ts +2 -0
  129. package/build/updates/index.js +96 -0
  130. package/build/upstream.d.ts +9 -0
  131. package/build/upstream.js +17 -0
  132. package/package.json +21 -5
  133. package/src/bin/expo-harmony.ts +2 -2
  134. package/src/buildHap/build.ts +13 -105
  135. package/src/buildHap/index.ts +19 -0
  136. package/src/cli.ts +27 -225
  137. package/src/command.ts +24 -0
  138. package/src/development/manifest.ts +106 -0
  139. package/src/development/protocol.ts +37 -0
  140. package/src/development/session.ts +83 -0
  141. package/src/doctor/doctor.ts +67 -62
  142. package/src/doctor/index.ts +16 -0
  143. package/src/exportEmbed/export.ts +73 -72
  144. package/src/exportEmbed/index.ts +17 -0
  145. package/src/file.ts +20 -11
  146. package/src/help.ts +41 -0
  147. package/src/index.ts +5 -3
  148. package/src/internal/development.ts +8 -0
  149. package/src/internal/export.ts +82 -0
  150. package/src/internal/prebuild.ts +3 -0
  151. package/src/log.ts +5 -0
  152. package/src/modules/index.ts +16 -0
  153. package/src/native/bare.ts +145 -0
  154. package/src/native/build.ts +83 -0
  155. package/src/{run → native}/cache.ts +10 -9
  156. package/src/native/hvigor.ts +88 -0
  157. package/src/native/install.ts +17 -0
  158. package/src/native/prepareProject.ts +99 -0
  159. package/src/native/project.ts +88 -0
  160. package/src/{tools.ts → native/toolchain.ts} +38 -140
  161. package/src/native/types.ts +44 -0
  162. package/src/prebuild/check.ts +56 -31
  163. package/src/prebuild/clean.ts +3 -3
  164. package/src/prebuild/index.ts +56 -0
  165. package/src/prebuild/prebuild.ts +11 -1
  166. package/src/prebuild/template.ts +2 -2
  167. package/src/process.ts +175 -64
  168. package/src/profile.ts +13 -0
  169. package/src/projectLock.ts +9 -1
  170. package/src/run/devices.ts +16 -11
  171. package/src/run/emulators.ts +4 -6
  172. package/src/run/index.ts +25 -0
  173. package/src/run/metro.ts +10 -1
  174. package/src/run/options.ts +2 -0
  175. package/src/run/run.ts +63 -129
  176. package/src/runtime/config.ts +150 -0
  177. package/src/runtime/contract.ts +165 -0
  178. package/src/runtime/fingerprint.ts +39 -0
  179. package/src/runtime/index.ts +54 -0
  180. package/src/start/index.ts +37 -0
  181. package/src/start/options.ts +12 -0
  182. package/src/updates/export.ts +60 -0
  183. package/src/updates/index.ts +89 -0
  184. package/src/buildHap/common.ts +0 -145
  185. package/src/run/install.ts +0 -41
@@ -1,39 +1,16 @@
1
- import path from 'node:path';
2
-
3
- import { HarmonyCliError } from '../errors';
4
- import { exportEmbedAsync } from '../exportEmbed/export';
5
- import type { HarmonyExportManifest } from '../exportEmbed/manifest';
6
- import { toPosixPath } from '../path';
7
1
  import { withHarmonyProjectLockAsync } from '../projectLock';
8
- import {
9
- commitHarmonyNativeBuildCacheAsync,
10
- prepareHarmonyNativeBuildCacheAsync,
11
- } from '../run/cache';
12
- import { installHarmonyDependenciesAsync } from '../run/install';
13
- import {
14
- resolveHarmonyBuildPlanAsync,
15
- resolveHarmonyToolchain,
16
- } from '../tools';
17
- import {
18
- ensureGeneratedProjectAsync,
19
- isNonEmptyRegularFile,
20
- progress,
21
- runCheckedAsync,
22
- timed,
23
- } from './common';
2
+ import { buildNativeAsync, type NativeBuildResult } from '../native/build';
3
+ import { prepareNativeProjectAsync } from '../native/prepareProject';
4
+ import { resolveHarmonyToolchain } from '../native/toolchain';
24
5
 
25
6
  export interface HarmonyBuildOptions {
26
7
  io?: Pick<Console, 'error' | 'log' | 'warn'>;
27
- /** @internal For release verification after a successful isolated prebuild check. */
28
- skipGeneratedProjectCheck?: boolean;
29
8
  sync?: boolean;
30
9
  variant?: 'debug' | 'release';
31
10
  }
32
11
 
33
- export interface HarmonyBuildResult {
12
+ export interface HarmonyBuildResult extends NativeBuildResult {
34
13
  bundleName: string;
35
- export: null | { assetCount: number; bundleSha256: string; sourceMapSha256: string };
36
- hapPath: string;
37
14
  headless: true;
38
15
  installed: false;
39
16
  launched: false;
@@ -48,13 +25,12 @@ type NormalizedBuildOptions = Required<HarmonyBuildOptions> & {
48
25
  };
49
26
 
50
27
  async function buildHarmonyUnlockedAsync(
51
- projectRoot: string,
28
+ root: string,
52
29
  options: HarmonyBuildOptions = {}
53
30
  ): Promise<HarmonyBuildResult> {
54
- const normalizedOptions: NormalizedBuildOptions = {
31
+ const settings: NormalizedBuildOptions = {
55
32
  io: options.io || console,
56
33
  requireDeviceTools: false,
57
- skipGeneratedProjectCheck: Boolean(options.skipGeneratedProjectCheck),
58
34
  sync: Boolean(options.sync),
59
35
  variant: options.variant || 'debug',
60
36
  };
@@ -66,98 +42,30 @@ async function buildHarmonyUnlockedAsync(
66
42
  metroPort: 0,
67
43
  };
68
44
 
69
- await ensureGeneratedProjectAsync(projectRoot, normalizedOptions, steps);
70
-
71
- const plan = await timed(steps, 'buildPlan', () => resolveHarmonyBuildPlanAsync(
72
- projectRoot,
73
- { buildMode: normalizedOptions.variant }
74
- ));
75
- const toolchain = resolveHarmonyToolchain();
76
-
77
- let exportManifest: HarmonyExportManifest | null = null;
78
- if (normalizedOptions.variant === 'release') {
79
- progress(normalizedOptions, 'Exporting the release Hermes bundle');
80
- exportManifest = await timed(steps, 'export', () => exportEmbedAsync(
81
- projectRoot,
82
- { skipDoctor: true }
83
- ));
84
- } else {
85
- steps.export = 0;
86
- }
87
-
88
- progress(normalizedOptions, 'Installing Harmony project dependencies');
89
- await timed(steps, 'ohpm', () => installHarmonyDependenciesAsync(plan, toolchain));
90
-
91
- progress(normalizedOptions, 'Checking Harmony native dependency cache');
92
- const nativeBuildCache = await timed(steps, 'nativeCache', () => (
93
- prepareHarmonyNativeBuildCacheAsync(projectRoot, plan)
94
- ));
95
-
96
- if (nativeBuildCache.changed) {
97
- progress(normalizedOptions, 'Invalidated stale Harmony native build objects');
98
- }
99
-
100
- progress(normalizedOptions, `Building the ${normalizedOptions.variant} HAP without a device`);
101
- const buildEnv = {
102
- ...process.env,
103
- EXPO_HARMONY_NODE: process.env.EXPO_HARMONY_NODE || process.execPath,
104
- EXPO_METRO_TARGET: 'harmony',
105
- HERMES_V1_ENABLED: 'true',
106
- ...(normalizedOptions.variant === 'release' ? { EXPO_HARMONY_BUNDLE_PREBUILT: '1' } : {}),
107
- ...(toolchain.sdkHome && !process.env.DEVECO_SDK_HOME
108
- ? { DEVECO_SDK_HOME: toolchain.sdkHome }
109
- : {}),
110
- };
111
- await timed(steps, 'build', () => runCheckedAsync(toolchain.hvigor.command, [
112
- ...toolchain.hvigor.args,
113
- ...plan.hvigorArgs,
114
- ], {
115
- code: 'ERR_HARMONY_BUILD_FAILED',
116
- cwd: plan.harmonyRoot,
117
- env: buildEnv,
118
- message: 'Hvigor build',
119
- operation: 'hvigor-build',
120
- timeoutMs: 15 * 60_000,
121
- }));
122
-
123
- if (!isNonEmptyRegularFile(plan.expectedHap)) {
124
- throw new HarmonyCliError(
125
- 'ERR_HARMONY_HAP_MISSING',
126
- 'Hvigor completed without producing the expected non-empty regular HAP.',
127
- { operation: 'verify-hap' }
128
- );
129
- }
130
-
131
- await timed(steps, 'nativeCacheCommit', () => commitHarmonyNativeBuildCacheAsync(nativeBuildCache));
45
+ const plan = await prepareNativeProjectAsync(root, settings, steps);
46
+ const built = await buildNativeAsync(root, plan, resolveHarmonyToolchain(), settings, steps);
132
47
 
133
48
  return {
134
49
  bundleName: plan.bundleName,
135
- export: exportManifest
136
- ? {
137
- assetCount: exportManifest.assets.length,
138
- bundleSha256: exportManifest.bundle.sha256,
139
- sourceMapSha256: exportManifest.sourceMap.sha256,
140
- }
141
- : null,
142
- hapPath: toPosixPath(path.relative(projectRoot, plan.expectedHap)),
50
+ ...built,
143
51
  headless: true,
144
52
  installed: false,
145
53
  launched: false,
146
54
  ok: true,
147
55
  schemaVersion: 1,
148
56
  steps,
149
- variant: normalizedOptions.variant,
57
+ variant: settings.variant,
150
58
  };
151
59
  }
152
60
 
153
61
  async function buildHarmonyAsync(
154
- projectRoot: string,
62
+ root: string,
155
63
  options: HarmonyBuildOptions = {}
156
64
  ): Promise<HarmonyBuildResult> {
157
65
  return withHarmonyProjectLockAsync(
158
- projectRoot,
66
+ root,
159
67
  'build',
160
- () => buildHarmonyUnlockedAsync(projectRoot, options)
68
+ () => buildHarmonyUnlockedAsync(root, options)
161
69
  );
162
70
  }
163
71
 
@@ -0,0 +1,19 @@
1
+ import { parseCommandArgs, type Command } from '../command';
2
+ import { parseBuildArgs } from './options';
3
+
4
+ export const command: Command = async (argv, io) => {
5
+ const args = parseCommandArgs(parseBuildArgs, argv, io);
6
+ if (!args) return 0;
7
+
8
+ const { projectRoot: root, options } = args;
9
+ const { buildHarmonyAsync } = await import('./build.js');
10
+
11
+ const result = await buildHarmonyAsync(root, {
12
+ ...options,
13
+ io,
14
+ });
15
+
16
+ io.log(`Built ${result.variant} HAP at ${result.hapPath}; no device actions were performed.`);
17
+
18
+ return 0;
19
+ };
package/src/cli.ts CHANGED
@@ -1,242 +1,44 @@
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
- `;
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
- if (invocation.command === 'start') {
100
- const { startExpoMetroAsync } = await import('./run/metro.js');
101
- const metro = await startExpoMetroAsync(invocation.projectRoot, {
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
- if (invocation.command === 'doctor') {
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
- return 0;
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 main(argv = process.argv.slice(2)) {
37
+ export async function mainAsync(argv: string[] = process.argv.slice(2)): Promise<void> {
229
38
  try {
230
- const code = await runAsync(argv);
231
-
232
- process.exitCode = code;
39
+ process.exitCode = await runAsync(argv);
233
40
  } catch (error) {
234
- const code = error.code || 'ERR_HARMONY_UNKNOWN';
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,106 @@
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 { 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 = `${bundle.origin}${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
+ // Expo installs the manifest before Metro's enhancer; adapt only the created instance.
84
+ BundlerDevServer.prototype.getManifestMiddlewareAsync = async function (this: object, ...args) {
85
+ const middleware = await create.apply(this, args);
86
+ const parse = middleware.getParsedHeaders;
87
+ const respond = middleware._getManifestResponseAsync;
88
+
89
+ middleware.getParsedHeaders = (request) => {
90
+ if (parsePlatformHeader(request) !== 'harmony') return parse.call(middleware, request);
91
+
92
+ // Bypass only the upstream platform assertion without changing the actual request.
93
+ const url = new URL(request.url, 'http://localhost');
94
+ url.searchParams.set('platform', 'ios');
95
+ const copy = Object.create(request);
96
+ copy.url = url.pathname + url.search;
97
+
98
+ return { ...parse.call(middleware, copy), platform: 'harmony' };
99
+ };
100
+ middleware._getManifestResponseAsync = options => options.platform === 'harmony'
101
+ ? createManifestResponseAsync(middleware, options, require)
102
+ : respond.call(middleware, options);
103
+
104
+ return middleware;
105
+ };
106
+ }
@@ -0,0 +1,37 @@
1
+ import { HarmonyCliError } from '../errors';
2
+
3
+ export const HarmonyManifestPath = '/manifest';
4
+
5
+ function validateManifestUrl(value: string): URL {
6
+ let url: URL;
7
+
8
+ try {
9
+ url = new URL(value);
10
+ } catch (cause) {
11
+ throw new HarmonyCliError(
12
+ 'ERR_HARMONY_MANIFEST_URL',
13
+ 'Expected an absolute HTTP(S) Harmony manifest URL.',
14
+ { cause, operation: 'development-manifest' }
15
+ );
16
+ }
17
+
18
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash
19
+ || !['/', '/manifest', '/index.exp'].includes(url.pathname) || url.searchParams.toString() !== 'platform=harmony') {
20
+ throw new HarmonyCliError(
21
+ 'ERR_HARMONY_MANIFEST_URL',
22
+ 'Expected an HTTP(S) Expo manifest URL with platform=harmony and without credentials or a fragment.',
23
+ { operation: 'development-manifest' }
24
+ );
25
+ }
26
+
27
+ return url;
28
+ }
29
+
30
+ export function createHarmonyLaunchLink(manifest: string): string {
31
+ validateManifestUrl(manifest);
32
+
33
+ const url = new URL('expo-harmony://open');
34
+ url.searchParams.set('url', manifest);
35
+
36
+ return url.toString();
37
+ }