@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
package/src/file.ts CHANGED
@@ -1,4 +1,4 @@
1
- import nodeCrypto from 'node:crypto';
1
+ import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
 
@@ -6,7 +6,7 @@ import { isInside } from './path';
6
6
 
7
7
  async function sha256File(file: string): Promise<string> {
8
8
  return await new Promise<string>((resolve, reject) => {
9
- const hash = nodeCrypto.createHash('sha256');
9
+ const hash = crypto.createHash('sha256');
10
10
  const input = fs.createReadStream(file);
11
11
 
12
12
  input.on('error', reject);
@@ -70,26 +70,26 @@ async function ensureSafeParent(root: string, destination: string): Promise<void
70
70
  async function atomicCopy(source: string, destination: string, root: string): Promise<void> {
71
71
  await ensureSafeParent(root, destination);
72
72
 
73
- const temporary = path.join(path.dirname(destination), `.${path.basename(destination)}.expo-${nodeCrypto.randomUUID()}.tmp`);
73
+ const temp = path.join(path.dirname(destination), `.${path.basename(destination)}.expo-${crypto.randomUUID()}.tmp`);
74
74
 
75
75
  try {
76
- await fs.promises.copyFile(source, temporary, fs.constants.COPYFILE_EXCL);
77
- await fs.promises.rename(temporary, destination);
76
+ await fs.promises.copyFile(source, temp, fs.constants.COPYFILE_EXCL);
77
+ await fs.promises.rename(temp, destination);
78
78
  } finally {
79
- await fs.promises.rm(temporary, { force: true });
79
+ await fs.promises.rm(temp, { force: true });
80
80
  }
81
81
  }
82
82
 
83
83
  async function atomicWriteJson(value: unknown, destination: string, root: string): Promise<void> {
84
84
  await ensureSafeParent(root, destination);
85
85
 
86
- const temporary = path.join(path.dirname(destination), `.${path.basename(destination)}.expo-${nodeCrypto.randomUUID()}.tmp`);
86
+ const temp = path.join(path.dirname(destination), `.${path.basename(destination)}.expo-${crypto.randomUUID()}.tmp`);
87
87
 
88
88
  try {
89
- await fs.promises.writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx' });
90
- await fs.promises.rename(temporary, destination);
89
+ await fs.promises.writeFile(temp, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx' });
90
+ await fs.promises.rename(temp, destination);
91
91
  } finally {
92
- await fs.promises.rm(temporary, { force: true });
92
+ await fs.promises.rm(temp, { force: true });
93
93
  }
94
94
  }
95
95
 
@@ -107,4 +107,13 @@ async function removeEmptyParents(file: string, stop: string): Promise<void> {
107
107
  }
108
108
  }
109
109
 
110
- export { atomicCopy, atomicWriteJson, describeFile, listFiles, removeEmptyParents };
110
+ function isNonEmptyRegularFile(file: string): boolean {
111
+ try {
112
+ const stat = fs.lstatSync(file);
113
+ return !stat.isSymbolicLink() && stat.isFile() && stat.size > 0;
114
+ } catch {
115
+ return false;
116
+ }
117
+ }
118
+
119
+ export { atomicCopy, atomicWriteJson, describeFile, isNonEmptyRegularFile, listFiles, removeEmptyParents };
package/src/help.ts ADDED
@@ -0,0 +1,41 @@
1
+ export const Help = `Usage: expo-harmony <command> [project] [options]
2
+
3
+ Commands:
4
+ start Start Expo Metro for Harmony development
5
+ runtime Describe project requirements; --client file checks compatibility
6
+ runtime --publish Embed capabilities from prepared native build inputs
7
+ prebuild Generate the Harmony native project with Expo CNG
8
+ prebuild --clean Safely recreate the managed Harmony directory
9
+ prebuild --check Compare generated desired state without project writes
10
+ build Build a HAP without selecting or contacting a device
11
+ doctor Validate config, versions, Metro, RNOH, SDK, and signing
12
+ modules list List native module candidates and their discovery source
13
+ modules inspect Resolve Harmony metadata (--package narrows the result)
14
+ modules verify Validate module config, registration conflicts and safe paths
15
+ export Export an OTA update, assets, metadata, and source map
16
+ export:embed Export validated Hermes bytecode, assets, and a source map
17
+ run Build, install, and launch the Harmony app
18
+
19
+ Options:
20
+ --no-install Skip dependency install (prebuild) or HAP install (run)
21
+ --npm|--yarn|--pnpm|--bun
22
+ Select the package manager used by prebuild dependency install
23
+ --skip-dependency-update <packages>
24
+ Preserve comma-separated dependency versions
25
+ --device <id-or-name> Select an HDC target or start a local emulator by name
26
+ --variant <mode> Build debug or release (default: debug)
27
+ --no-bundler Use an already-running Expo Metro server
28
+ --app-id <bundleName>
29
+ Launch another installed app (requires --no-install when different)
30
+ --host <hostname> Advertised HTTP LAN hostname (start)
31
+ --private-key-path <path>
32
+ Sign development manifests using the configured Updates certificate
33
+ --port <number> Metro and device reverse port (default: 8081)
34
+ --sync Re-run prebuild before building
35
+ --output-dir <path> OTA export directory (default: dist)
36
+ --asset-url <url> HTTP(S) root hosting exported OTA assets
37
+ --check Validate an existing export without writing
38
+ --reset-cache Reset Metro while exporting or starting
39
+ -c, --clear Alias for --reset-cache (start)
40
+ -h, --help Show this help
41
+ `;
package/src/index.ts CHANGED
@@ -2,11 +2,13 @@ export { runAsync } from './cli';
2
2
  export { buildHarmonyAsync } from './buildHap/build';
3
3
  export type { HarmonyBuildOptions, HarmonyBuildResult } from './buildHap/build';
4
4
  export { exportEmbedAsync } from './exportEmbed/export';
5
- export { resolveHarmonyBuildPlanAsync, resolveHarmonyToolchain } from './tools';
6
- export { installHarmonyDependenciesAsync } from './run/install';
5
+ export { resolveHarmonyBuildPlanAsync } from './native/project';
6
+ export type { HarmonyBuildPlan, HarmonyBuildMode } from './native/types';
7
+ export { resolveHarmonyToolchain } from './native/toolchain';
8
+ export { installHarmonyDependenciesAsync } from './native/install';
7
9
  export { formatModulesResult, runModulesCommandAsync } from './modules/modules';
8
10
  export type { ModulesCommandResult } from './modules/modules';
9
- export { commitHarmonyNativeBuildCacheAsync, prepareHarmonyNativeBuildCacheAsync } from './run/cache';
11
+ export { commitHarmonyNativeBuildCacheAsync, prepareHarmonyNativeBuildCacheAsync } from './native/cache';
10
12
  export { runHarmonyAsync } from './run/run';
11
13
  export type { HarmonyExportManifest } from './exportEmbed/manifest';
12
14
  export type { HarmonyRunOptions, HarmonyRunResult } from './run/run';
@@ -0,0 +1,8 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ import { installHarmonyManifest } from '../development/manifest';
4
+
5
+ installHarmonyManifest(process.cwd());
6
+ process.argv.splice(1, 1);
7
+ const run = createRequire(__filename);
8
+ run(process.argv[1]);
@@ -0,0 +1,82 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import { createRequire } from 'node:module';
4
+ import path from 'node:path';
5
+ import { HarmonyCliError } from '../errors';
6
+
7
+ async function main(): Promise<void> {
8
+ const [root, serialized, destination] = process.argv.slice(2);
9
+ const project = createRequire(path.join(root, 'package.json'));
10
+ const expo = createRequire(project.resolve('expo/package.json'));
11
+ const require = createRequire(expo.resolve('@expo/cli/package.json'));
12
+ if (require('./package.json').version !== '55.0.32') {
13
+ throw new HarmonyCliError('ERR_HARMONY_EXPORT_UPSTREAM', 'Harmony asset export requires Expo CLI 55.0.32.', { operation: 'export' });
14
+ }
15
+
16
+ const options = JSON.parse(serialized);
17
+ require('./build/src/utils/nodeEnv').setNodeEnv('production');
18
+ require('./build/src/utils/nodeEnv').loadEnvFiles(root);
19
+ if (options.update) {
20
+ const directory = path.join(path.dirname(destination), 'export');
21
+ await require('./build/src/export/exportApp').exportAppAsync(root, {
22
+ platforms: ['harmony'], outputDir: directory, clear: options.resetCache,
23
+ dev: false, sourceMaps: true, minify: false, bytecode: false,
24
+ });
25
+ const metadata = JSON.parse(await fs.readFile(path.join(directory, 'metadata.json'), 'utf8')).fileMetadata.harmony;
26
+ const mime = require('mime-types');
27
+ const assets = [];
28
+ for (const asset of metadata.assets) {
29
+ const source = path.join(directory, asset.path);
30
+ const data = await fs.readFile(source);
31
+ const key = path.basename(asset.path, path.extname(asset.path));
32
+ const relative = `assets/${key}.${asset.ext}`;
33
+ await fs.mkdir(path.join(options.assetsDest, 'assets'), { recursive: true });
34
+ await fs.copyFile(source, path.join(options.assetsDest, relative));
35
+ assets.push({ key, hash: crypto.createHash('sha256').update(Uint8Array.from(data)).digest('base64url'),
36
+ contentType: mime.lookup(asset.ext) || 'application/octet-stream', fileExtension: '.' + asset.ext,
37
+ embeddedAssetFilename: relative });
38
+ }
39
+ await require('@expo/metro/metro/shared/output/bundle').save({
40
+ code: await fs.readFile(path.join(directory, metadata.bundle), 'utf8'),
41
+ map: await fs.readFile(path.join(directory, metadata.bundle + '.map'), 'utf8'),
42
+ }, options, console.log);
43
+ await fs.writeFile(destination, JSON.stringify(assets));
44
+
45
+ return;
46
+ }
47
+
48
+ const { bundle, assets, files } = await require('./build/src/export/embed/exportEmbedAsync').exportEmbedBundleAndAssetsAsync(root, options);
49
+ const { getAssetLocalPath } = require('./build/src/export/metroAssetLocalPath');
50
+ const mime = require('mime-types');
51
+ const metadata: Record<string, unknown>[] = [];
52
+
53
+ for (const asset of assets) {
54
+ if (!Array.isArray(asset.fileHashes) || asset.fileHashes.length !== asset.files.length) {
55
+ throw new HarmonyCliError('ERR_HARMONY_EXPORT_ASSETS', 'Metro assets must include expo-asset fileHashes.', { operation: 'export' });
56
+ }
57
+ for (let index = 0; index < asset.files.length; index++) {
58
+ const data = await fs.readFile(asset.files[index]);
59
+ metadata.push({ key: asset.fileHashes[index], hash: crypto.createHash('sha256').update(Uint8Array.from(data)).digest('base64url'),
60
+ contentType: mime.lookup(asset.type) || 'application/octet-stream', fileExtension: '.' + asset.type,
61
+ embeddedAssetFilename: getAssetLocalPath(asset, { platform: 'harmony', scale: asset.scales[index] }) });
62
+ }
63
+ }
64
+
65
+ await fs.mkdir(path.dirname(options.bundleOutput), { recursive: true });
66
+ await Promise.all([
67
+ require('@expo/metro/metro/shared/output/bundle').save(bundle, options, console.log),
68
+ require('./build/src/export/persistMetroAssets').persistMetroAssetsAsync(root, assets, { platform: 'harmony', outputDirectory: options.assetsDest }),
69
+ ]);
70
+ if (files.size > 0) {
71
+ const { copyPublicFolderAsync, getPublicFolderPath } = require('./build/src/export/publicFolder');
72
+ const { DOM_COMPONENTS_BUNDLE_DIR } = require('./build/src/start/server/middleware/DomComponentsMiddleware');
73
+ await require('./build/src/export/saveAssets').persistMetroFilesAsync(files, options.assetsDest);
74
+ await copyPublicFolderAsync(getPublicFolderPath(root), path.join(options.assetsDest, DOM_COMPONENTS_BUNDLE_DIR));
75
+ }
76
+ await fs.writeFile(destination, JSON.stringify(metadata));
77
+ }
78
+
79
+ main().catch((error) => {
80
+ console.error(error);
81
+ process.exitCode = 1;
82
+ });
@@ -0,0 +1,3 @@
1
+ export { withGeneratedProjectAsync } from '../prebuild/check';
2
+ export { assertSafeCleanTarget } from '../prebuild/clean';
3
+ export { withHarmonyProjectLockAsync } from '../projectLock';
package/src/log.ts ADDED
@@ -0,0 +1,5 @@
1
+ export type Log = Pick<Console, 'error' | 'log' | 'warn'>;
2
+
3
+ export function progress(options: { io?: Log }, message: string): void {
4
+ options.io?.log(`› ${message}`);
5
+ }
@@ -0,0 +1,16 @@
1
+ import { parseCommandArgs, type Command } from '../command';
2
+ import { parseModulesArgs } from './options';
3
+
4
+ export const command: Command = async (argv, io) => {
5
+ const args = parseCommandArgs(parseModulesArgs, argv, io);
6
+ if (!args) return 0;
7
+
8
+ const { projectRoot: root, options } = args;
9
+ const { formatModulesResult, runModulesCommandAsync } = await import('./modules.js');
10
+
11
+ const result = await runModulesCommandAsync(root, options);
12
+
13
+ io.log(formatModulesResult(result));
14
+
15
+ return result.ok ? 0 : 1;
16
+ };
@@ -0,0 +1,145 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import JSON5 from 'json5';
4
+
5
+ import { HarmonyCliError } from '../errors';
6
+ import type { BareHarmonyBuildPlan } from './types';
7
+
8
+ function isBareHarmonyProject(root: string): boolean {
9
+ return fs.existsSync(path.join(root, 'harmony'))
10
+ && !fs.existsSync(path.join(root, 'harmony/.expo-harmony-template'))
11
+ && !fs.existsSync(path.join(root, '.expo/harmony/cng-manifest.json'));
12
+ }
13
+
14
+ function record(value: unknown, field: string): Record<string, unknown> {
15
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
16
+ throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', `Native ${field} must be an object.`, { operation: 'resolve-build' });
17
+ }
18
+
19
+ return value as Record<string, unknown>;
20
+ }
21
+
22
+ function records(value: unknown, field: string): Record<string, unknown>[] {
23
+ if (!Array.isArray(value)) {
24
+ throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', `Native ${field} must be an array.`, { operation: 'resolve-build' });
25
+ }
26
+
27
+ return value.map(item => record(item, field));
28
+ }
29
+
30
+ function readJson(file: string): Record<string, unknown> {
31
+ try {
32
+ return record(JSON5.parse(fs.readFileSync(file, 'utf8')), file);
33
+ } catch (cause) {
34
+ throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', `Cannot read native project file ${file}: ${cause.message}`, { cause, operation: 'resolve-build' });
35
+ }
36
+ }
37
+
38
+ function identifier(value: unknown, field: string): string {
39
+ if (typeof value !== 'string' || !/^[A-Za-z_][A-Za-z0-9_.]*$/.test(value)) {
40
+ throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', `The native project must specify a valid ${field}.`, { operation: 'resolve-build' });
41
+ }
42
+
43
+ return value;
44
+ }
45
+
46
+ function selectDefault(items: Record<string, unknown>[], field: string): Record<string, unknown> {
47
+ if (items.length === 0) {
48
+ throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', `No ${field} is defined in harmony/build-profile.json5.`, { operation: 'resolve-build' });
49
+ }
50
+
51
+ const defaults = items.filter(item => item.name === 'default');
52
+
53
+ if (defaults.length === 1) return defaults[0];
54
+ if (items.length === 1) return items[0];
55
+
56
+ throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', `Multiple ${field} are defined; name the intended default "default" in harmony/build-profile.json5.`, { operation: 'resolve-build' });
57
+ }
58
+
59
+ function resolveBareHarmonyBuildPlan(root: string, mode: 'debug' | 'release'): BareHarmonyBuildPlan {
60
+ const harmony = path.join(root, 'harmony');
61
+ const entry = path.join(harmony, 'entry');
62
+
63
+ for (const directory of [harmony, entry]) {
64
+ if (!fs.existsSync(directory)
65
+ || !fs.lstatSync(directory).isDirectory() || fs.lstatSync(directory).isSymbolicLink()) {
66
+ throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', `Bare Harmony projects require a regular directory at ${directory}.`, { operation: 'resolve-build' });
67
+ }
68
+ }
69
+
70
+ const profile = readJson(path.join(harmony, 'build-profile.json5'));
71
+ const app = record(readJson(path.join(harmony, 'AppScope/app.json5')).app, 'app');
72
+ const module = record(readJson(path.join(entry, 'src/main/module.json5')).module, 'module');
73
+
74
+ const name = identifier(module.name, 'module.name');
75
+ const registration = records(profile.modules, 'modules').find(item => item.name === name);
76
+ if (!registration || typeof registration.srcPath !== 'string'
77
+ || path.resolve(harmony, registration.srcPath) !== entry || module.type !== 'entry') {
78
+ throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', 'Bare Harmony autolinking requires an entry module at harmony/entry, registered in build-profile.json5.', { operation: 'resolve-build' });
79
+ }
80
+
81
+ const config = record(profile.app, 'build-profile.app');
82
+ const selected = selectDefault(records(config.products, 'products'), 'products');
83
+ const product = identifier(selected.name, 'product name');
84
+ const targets = records(registration.targets, 'module targets').filter((item) => {
85
+ if (item.applyToProducts === undefined) return true;
86
+ if (!Array.isArray(item.applyToProducts) || item.applyToProducts.some(value => typeof value !== 'string')) {
87
+ throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', 'Native target.applyToProducts must be an array of product names.', { operation: 'resolve-build' });
88
+ }
89
+
90
+ return item.applyToProducts.includes(product);
91
+ });
92
+ const target = identifier(selectDefault(targets, 'module targets').name, 'target name');
93
+
94
+ const ability = identifier(module.mainElement, 'module.mainElement');
95
+ if (!records(module.abilities, 'abilities').some(item => item.name === ability)) {
96
+ throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', `Ability ${ability} is missing from module.json5.`, { operation: 'resolve-build' });
97
+ }
98
+
99
+ const bundle = identifier(app.bundleName, 'app.bundleName');
100
+ const signed = Boolean(selected.signingConfig);
101
+ if (signed && !records(config.signingConfigs, 'signingConfigs').some(item => item.name === selected.signingConfig)) {
102
+ throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', `Signing config ${selected.signingConfig} is referenced by the product but is not defined.`, { operation: 'resolve-build' });
103
+ }
104
+
105
+ const rawfile = path.join(entry, 'src/main/resources/rawfile');
106
+ const metadata = path.join(root, '.expo/harmony/export');
107
+
108
+ return {
109
+ workflow: 'bare',
110
+ abilityName: ability,
111
+ buildMode: mode,
112
+ bundleName: bundle,
113
+ harmonyRoot: harmony,
114
+ moduleName: name,
115
+ moduleRoot: entry,
116
+ productName: product,
117
+ targetName: target,
118
+ expectedHap: path.join(entry, `build/${target}/outputs/${target}/${name}-${target}-${signed ? 'signed' : 'unsigned'}.hap`),
119
+ hvigorArgs: ['--mode', 'module', '-p', `module=${name}@${target}`, '-p', `product=${product}`, '-p', `buildMode=${mode}`, '--no-daemon', 'assembleHap'],
120
+ exportPaths: {
121
+ bundle: path.join(rawfile, 'hermes_bundle.hbc'),
122
+ manifest: path.join(root, '.expo/harmony/export-manifest.json'),
123
+ metadataRoot: metadata,
124
+ rawfileRoot: rawfile,
125
+ sourceMap: path.join(metadata, 'hermes_bundle.hbc.map'),
126
+ },
127
+ nativeCache: {
128
+ invalidationRoots: [path.join(entry, '.cxx'), path.join(entry, 'build')],
129
+ stateFile: path.join(entry, '.cxx/.expo-harmony-native-dependencies.json'),
130
+ },
131
+ nativeInputs: {
132
+ lockfile: path.join(harmony, 'oh-package-lock.json5'),
133
+ manifest: path.join(harmony, 'oh-package.json5'),
134
+ },
135
+ projectFiles: {
136
+ hvigorConfig: path.join(harmony, 'hvigor/hvigor-config.json5'),
137
+ moduleHvigor: path.join(entry, 'hvigorfile.ts'),
138
+ moduleJson: path.join(entry, 'src/main/module.json5'),
139
+ projectBuildProfile: path.join(harmony, 'build-profile.json5'),
140
+ rootHvigor: path.join(harmony, 'hvigorfile.ts'),
141
+ },
142
+ };
143
+ }
144
+
145
+ export { isBareHarmonyProject, resolveBareHarmonyBuildPlan };
@@ -0,0 +1,83 @@
1
+ import { getConfig } from '@expo/config';
2
+ import path from 'node:path';
3
+
4
+ import { commitHarmonyNativeBuildCacheAsync, prepareHarmonyNativeBuildCacheAsync } from './cache';
5
+ import { installHarmonyDependenciesAsync } from './install';
6
+ import type { HarmonyToolchain } from './toolchain';
7
+ import type { HarmonyBuildPlan } from './types';
8
+ import { HarmonyCliError } from '../errors';
9
+ import { exportEmbedAsync } from '../exportEmbed/export';
10
+ import { isNonEmptyRegularFile } from '../file';
11
+ import { progress, type Log } from '../log';
12
+ import { toPosixPath } from '../path';
13
+ import { runCheckedAsync } from '../process';
14
+ import { timedAsync } from '../profile';
15
+
16
+ export interface NativeBuildResult {
17
+ export: null | { assetCount: number; bundleSha256: string; sourceMapSha256: string };
18
+ hapPath: string;
19
+ }
20
+
21
+ export async function buildNativeAsync(
22
+ root: string,
23
+ plan: HarmonyBuildPlan,
24
+ tools: HarmonyToolchain,
25
+ options: { io?: Log; resetCache?: boolean },
26
+ steps: Record<string, number>
27
+ ): Promise<NativeBuildResult> {
28
+ let exported: NativeBuildResult['export'] = null;
29
+ const embedded = plan.buildMode === 'release' || getConfig(root).exp.updates?.useNativeDebug === true;
30
+ if (embedded) {
31
+ progress(options, 'Exporting the release Hermes bundle');
32
+ const manifest = await timedAsync(steps, 'export', () => exportEmbedAsync(root, {
33
+ resetCache: options.resetCache,
34
+ skipDoctor: true,
35
+ }));
36
+
37
+ exported = {
38
+ assetCount: manifest.assets.length,
39
+ bundleSha256: manifest.bundle.sha256,
40
+ sourceMapSha256: manifest.sourceMap.sha256,
41
+ };
42
+ } else {
43
+ steps.export = 0;
44
+ }
45
+
46
+ progress(options, 'Installing Harmony project dependencies');
47
+ await timedAsync(steps, 'ohpm', () => installHarmonyDependenciesAsync(plan, tools));
48
+
49
+ progress(options, 'Checking Harmony native dependency cache');
50
+ const cache = await timedAsync(steps, 'nativeCache', () => prepareHarmonyNativeBuildCacheAsync(root, plan));
51
+ if (cache.changed) progress(options, 'Invalidated stale Harmony native build objects');
52
+
53
+ progress(options, `Building the ${plan.buildMode} HAP`);
54
+ await timedAsync(steps, 'build', () => runCheckedAsync(tools.hvigor.command, [
55
+ ...tools.hvigor.args,
56
+ ...plan.hvigorArgs,
57
+ ], {
58
+ code: 'ERR_HARMONY_BUILD_FAILED',
59
+ cwd: plan.harmonyRoot,
60
+ env: {
61
+ ...process.env,
62
+ EXPO_HARMONY_NODE: process.env.EXPO_HARMONY_NODE || process.execPath,
63
+ EXPO_METRO_TARGET: 'harmony',
64
+ HERMES_V1_ENABLED: 'true',
65
+ ...(plan.workflow === 'bare' ? { EXPO_HARMONY_NATIVE_PREPARED: '1' } : {}),
66
+ ...(embedded ? { EXPO_HARMONY_BUNDLE_PREBUILT: '1' } : {}),
67
+ ...(tools.sdkHome && !process.env.DEVECO_SDK_HOME
68
+ ? { DEVECO_SDK_HOME: tools.sdkHome }
69
+ : {}),
70
+ },
71
+ message: 'Hvigor build',
72
+ operation: 'hvigor-build',
73
+ timeoutMs: 15 * 60_000,
74
+ }));
75
+
76
+ if (!isNonEmptyRegularFile(plan.expectedHap)) {
77
+ throw new HarmonyCliError('ERR_HARMONY_HAP_MISSING', 'Hvigor completed without producing the expected non-empty regular HAP.', { operation: 'verify-hap' });
78
+ }
79
+
80
+ await timedAsync(steps, 'nativeCacheCommit', () => commitHarmonyNativeBuildCacheAsync(cache));
81
+
82
+ return { export: exported, hapPath: toPosixPath(path.relative(root, plan.expectedHap)) };
83
+ }
@@ -4,10 +4,10 @@ import path from 'node:path';
4
4
  import {
5
5
  HarmonyNativeInputsFingerprintVersion,
6
6
  fingerprintHarmonyNativeInputsSync,
7
- } from '@expo-harmony/config-plugins/native-inputs';
7
+ } from '@expo-harmony/config-plugins/internal/native-inputs';
8
8
 
9
9
  import { HarmonyCliError } from '../errors';
10
- import type { HarmonyBuildPlan } from '../tools';
10
+ import { type HarmonyBuildPlan } from './types';
11
11
 
12
12
  export interface HarmonyNativeBuildCacheState {
13
13
  artifactCount: number;
@@ -17,13 +17,14 @@ export interface HarmonyNativeBuildCacheState {
17
17
  fingerprintVersion: number;
18
18
  }
19
19
 
20
- const CacheSchemaVersion = 1;
20
+ const SchemaVersion = 1;
21
21
 
22
22
  async function readOptionalFile(file) {
23
23
  try {
24
24
  return await fs.promises.readFile(file);
25
25
  } catch (error) {
26
26
  if (error?.code === 'ENOENT') return null;
27
+
27
28
  throw new HarmonyCliError(
28
29
  error.code || 'ERR_HARMONY_NATIVE_CACHE',
29
30
  error.message || `Cannot read a Harmony native cache input: ${file}`,
@@ -33,14 +34,14 @@ async function readOptionalFile(file) {
33
34
  }
34
35
 
35
36
  async function resolveNativeDependencyFingerprintAsync(
36
- projectRoot: string,
37
+ root: string,
37
38
  plan: HarmonyBuildPlan
38
39
  ) {
39
40
  try {
40
41
  return fingerprintHarmonyNativeInputsSync({
41
42
  lockfile: plan.nativeInputs.lockfile,
42
43
  manifest: plan.nativeInputs.manifest,
43
- projectRoot,
44
+ projectRoot: root,
44
45
  });
45
46
  } catch (cause) {
46
47
  throw new HarmonyCliError(
@@ -52,10 +53,10 @@ async function resolveNativeDependencyFingerprintAsync(
52
53
  }
53
54
 
54
55
  async function prepareHarmonyNativeBuildCacheAsync(
55
- projectRoot: string,
56
+ root: string,
56
57
  plan: HarmonyBuildPlan
57
58
  ): Promise<HarmonyNativeBuildCacheState> {
58
- const current = await resolveNativeDependencyFingerprintAsync(projectRoot, plan);
59
+ const current = await resolveNativeDependencyFingerprintAsync(root, plan);
59
60
  const file = plan.nativeCache.stateFile;
60
61
  const source = await readOptionalFile(file);
61
62
  let saved = null;
@@ -68,7 +69,7 @@ async function prepareHarmonyNativeBuildCacheAsync(
68
69
  }
69
70
  }
70
71
 
71
- const changed = saved?.schemaVersion !== CacheSchemaVersion
72
+ const changed = saved?.schemaVersion !== SchemaVersion
72
73
  || saved?.fingerprintVersion !== HarmonyNativeInputsFingerprintVersion
73
74
  || saved?.fingerprint !== current.fingerprint;
74
75
 
@@ -94,7 +95,7 @@ async function commitHarmonyNativeBuildCacheAsync(state: HarmonyNativeBuildCache
94
95
  artifactCount: state.artifactCount,
95
96
  fingerprint: state.fingerprint,
96
97
  fingerprintVersion: state.fingerprintVersion,
97
- schemaVersion: CacheSchemaVersion,
98
+ schemaVersion: SchemaVersion,
98
99
  }, null, 2)}\n`);
99
100
  await fs.promises.rename(temp, state.cacheFile);
100
101
  }
@@ -0,0 +1,88 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import path from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { fingerprintHarmonyNativeInputsSync } from '@expo-harmony/config-plugins/internal/native-inputs';
7
+
8
+ import { HarmonyCliError } from '../errors';
9
+ import { createHarmonyToolchainEnv, resolveHarmonyToolchain } from './toolchain';
10
+
11
+ function nodeExecutable(): string {
12
+ const candidates = process.env.EXPO_HARMONY_NODE
13
+ ? [process.env.EXPO_HARMONY_NODE]
14
+ : [
15
+ process.execPath,
16
+ ...(process.env.PATH || '').split(path.delimiter).filter(Boolean)
17
+ .map(directory => path.join(directory, process.platform === 'win32' ? 'node.exe' : 'node')),
18
+ ];
19
+
20
+ for (const candidate of new Set(candidates)) {
21
+ const result = spawnSync(candidate, ['-p', 'process.versions.node'], { encoding: 'utf8', timeout: 5_000 });
22
+ if (result.status === 0 && Number.parseInt(result.stdout, 10) >= 20) return candidate;
23
+ }
24
+
25
+ throw new HarmonyCliError('ERR_HARMONY_NODE', 'Harmony native builds require Node.js 20 or newer. Set EXPO_HARMONY_NODE to its executable.', { operation: 'native-build' });
26
+ }
27
+
28
+ function run(command: string, args: string[], cwd: string): void {
29
+ const result = spawnSync(command, args, {
30
+ cwd,
31
+ env: { ...createHarmonyToolchainEnv(), EXPO_HARMONY: '1', EXPO_METRO_TARGET: 'harmony', HERMES_V1_ENABLED: 'true' },
32
+ stdio: 'inherit',
33
+ timeout: 10 * 60_000,
34
+ });
35
+
36
+ if (result.error || result.status !== 0) {
37
+ throw new HarmonyCliError('ERR_HARMONY_NATIVE_BUILD', `Harmony build command failed: ${command} (${result.error?.message || result.status}).`, { cause: result.error, exitCode: result.status || 1, operation: 'native-build' });
38
+ }
39
+ }
40
+
41
+ // Hvigor configuration is synchronous; linking must finish before task execution.
42
+ export function prepareHarmonyNativeBuild(root: string, mode: 'debug' | 'release'): void {
43
+ const harmony = path.join(root, 'harmony');
44
+
45
+ if (process.env.EXPO_HARMONY_NATIVE_PREPARED !== '1') {
46
+ const require = createRequire(path.join(root, 'package.json'));
47
+ const manifest = path.resolve(path.dirname(require.resolve('@expo-harmony/expo-modules-autolinking')), '../package.json');
48
+ const bin = path.resolve(path.dirname(manifest), require(manifest).bin['expo-harmony-autolinking']);
49
+
50
+ run(nodeExecutable(), [bin, 'link', '--project-root', root, '--harmony-project-path', harmony, '--build-type', mode], root);
51
+
52
+ const { ohpm } = resolveHarmonyToolchain();
53
+ run(ohpm.command, [...ohpm.args, 'install', '--all'], harmony);
54
+ }
55
+
56
+ publishHarmonyNativeRuntime(root, mode);
57
+
58
+ const { fingerprint } = fingerprintHarmonyNativeInputsSync({
59
+ projectRoot: root,
60
+ manifest: path.join(harmony, 'oh-package.json5'),
61
+ lockfile: path.join(harmony, 'oh-package-lock.json5'),
62
+ });
63
+ const file = path.join(harmony, '.hvigor/expo-harmony/native-inputs.cmake');
64
+ const content = `# Generated native dependency fingerprint.\nadd_compile_definitions(EXPO_HARMONY_NATIVE_INPUTS_SHA256_${fingerprint}=1)\n`;
65
+ if (fs.existsSync(file) && fs.readFileSync(file, 'utf8') === content) return;
66
+
67
+ fs.mkdirSync(path.dirname(file), { recursive: true });
68
+ const temp = `${file}.${crypto.randomUUID()}.tmp`;
69
+
70
+ try {
71
+ fs.writeFileSync(temp, content);
72
+ fs.renameSync(temp, file);
73
+ } finally {
74
+ fs.rmSync(temp, { force: true });
75
+ }
76
+ }
77
+
78
+ export function bundleHarmonyNativeBuild(root: string): void {
79
+ if (process.env.EXPO_HARMONY_BUNDLE_PREBUILT === '1') return;
80
+
81
+ const require = createRequire(path.join(root, 'package.json'));
82
+ run(nodeExecutable(), [require.resolve('@expo-harmony/cli/bin/expo-harmony'), 'export:embed', root], root);
83
+ }
84
+
85
+ export function publishHarmonyNativeRuntime(root: string, mode: 'debug' | 'release'): void {
86
+ const require = createRequire(path.join(root, 'package.json'));
87
+ run(nodeExecutable(), [require.resolve('@expo-harmony/cli/bin/expo-harmony'), 'runtime', root, '--publish', '--variant', mode], root);
88
+ }
@@ -0,0 +1,17 @@
1
+ import type { HarmonyBuildPlan } from './types';
2
+ import type { HarmonyToolchain } from './toolchain';
3
+ import { runCheckedAsync } from '../process';
4
+
5
+ export async function installHarmonyDependenciesAsync(
6
+ plan: Pick<HarmonyBuildPlan, 'harmonyRoot'>,
7
+ tools: HarmonyToolchain,
8
+ options: { timeoutMs?: number } = {}
9
+ ): Promise<void> {
10
+ await runCheckedAsync(tools.ohpm.command, [...tools.ohpm.args, 'install', '--all'], {
11
+ code: 'ERR_HARMONY_OHPM_FAILED',
12
+ cwd: plan.harmonyRoot,
13
+ message: 'OHPM install',
14
+ operation: 'ohpm-install',
15
+ timeoutMs: options.timeoutMs || 5 * 60_000,
16
+ });
17
+ }