@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
@@ -0,0 +1,99 @@
1
+ import fs from 'node:fs';
2
+ import { linkModulesAsync } from '@expo-harmony/expo-modules-autolinking';
3
+
4
+ import { doctorAsync } from '../doctor/doctor';
5
+ import { HarmonyCliError } from '../errors';
6
+ import { progress, type Log } from '../log';
7
+ import { timedAsync } from '../profile';
8
+ import { checkAsync } from '../prebuild/check';
9
+ import { prebuildParsedAsync } from '../prebuild/prebuild';
10
+ import { createHarmonyToolchainEnv } from './toolchain';
11
+ import { resolveHarmonyBuildPlanAsync, resolveHarmonyBuildPlanIfPresentAsync } from './project';
12
+ import type { HarmonyBuildPlan } from './types';
13
+
14
+ interface PrepareProjectOptions {
15
+ io?: Log;
16
+ requireDeviceTools?: boolean;
17
+ check?: boolean;
18
+ sync: boolean;
19
+ variant: 'debug' | 'release';
20
+ }
21
+
22
+ async function prepareNativeProjectAsync(
23
+ root: string,
24
+ options: PrepareProjectOptions,
25
+ steps: Record<string, number>
26
+ ): Promise<HarmonyBuildPlan> {
27
+ const plan = await timedAsync(steps, 'buildPlan', () => resolveHarmonyBuildPlanIfPresentAsync(root, {
28
+ buildMode: options.variant,
29
+ }));
30
+ const exists = Boolean(plan && fs.existsSync(plan.harmonyRoot));
31
+
32
+ if (plan?.workflow === 'bare' && options.sync) {
33
+ throw new HarmonyCliError('ERR_HARMONY_BARE_SYNC', '--sync regenerates CNG projects and cannot be used with a manually maintained bare project.', { operation: 'build' });
34
+ }
35
+
36
+ progress(options, 'Checking the Harmony project');
37
+ const doctor = await timedAsync(steps, 'doctor', () => doctorAsync(root, {
38
+ requireBuildTools: true,
39
+ requireDeviceTools: options.requireDeviceTools,
40
+ validateGeneratedProject: exists,
41
+ validateModules: false,
42
+ }));
43
+ if (!doctor.ok) {
44
+ const checks = doctor.checks.filter(check => check.status === 'error').map(check => check.id);
45
+
46
+ throw new HarmonyCliError('ERR_HARMONY_DOCTOR_FAILED', `Harmony doctor found blocking checks: ${checks.join(', ') || 'unknown'}.`, { operation: 'doctor' });
47
+ }
48
+
49
+ if (plan?.workflow === 'bare') {
50
+ progress(options, 'Autolinking the bare Harmony project');
51
+ await timedAsync(steps, 'autolinking', () => linkModulesAsync({
52
+ projectRoot: root,
53
+ harmonyProjectPath: plan.harmonyRoot,
54
+ buildType: options.variant,
55
+ env: createHarmonyToolchainEnv(),
56
+ }));
57
+
58
+ steps.prebuild = 0;
59
+ steps.cngCheck = 0;
60
+
61
+ return plan;
62
+ }
63
+
64
+ if (!exists || options.sync) {
65
+ progress(options, exists
66
+ ? 'Synchronizing the generated Harmony project'
67
+ : 'Generating the missing Harmony project');
68
+ await timedAsync(steps, 'prebuild', () => prebuildParsedAsync(
69
+ root,
70
+ [],
71
+ { buildType: options.variant }
72
+ ));
73
+ steps.cngCheck = 0;
74
+
75
+ return timedAsync(steps, 'buildPlan', () => resolveHarmonyBuildPlanAsync(root, { buildMode: options.variant }));
76
+ }
77
+
78
+ steps.prebuild = 0;
79
+ if (options.check === false) {
80
+ steps.cngCheck = 0;
81
+ return plan;
82
+ }
83
+
84
+ progress(options, 'Checking CNG ownership and drift');
85
+ const check = await timedAsync(steps, 'cngCheck', () => checkAsync(root, { buildType: options.variant }));
86
+ if (!check.clean) {
87
+ const summary = check.changes.slice(0, 8).map(change => `${change.type}:${change.path}`).join(', ');
88
+
89
+ throw new HarmonyCliError(
90
+ 'ERR_HARMONY_MANIFEST_DRIFT',
91
+ `Generated Harmony files differ from CNG desired state${summary ? ` (${summary})` : ''}. Run expo-harmony prebuild or retry with --sync.`,
92
+ { operation: 'check' }
93
+ );
94
+ }
95
+
96
+ return plan;
97
+ }
98
+
99
+ export { prepareNativeProjectAsync };
@@ -0,0 +1,88 @@
1
+ import { readManifestIfPresentAsync, resolveHarmonyBuildPath, type HarmonyBuildDescriptor } from '@expo-harmony/prebuild-config/internal';
2
+
3
+ import { HarmonyCliError } from '../errors';
4
+ import { isBareHarmonyProject, resolveBareHarmonyBuildPlan } from './bare';
5
+ import type { CngHarmonyBuildPlan, HarmonyBuildPlan } from './types';
6
+
7
+ function createHarmonyBuildPlan(
8
+ root: string,
9
+ build: HarmonyBuildDescriptor,
10
+ mode: 'debug' | 'release'
11
+ ): CngHarmonyBuildPlan {
12
+ const resolve = relative => resolveHarmonyBuildPath(root, relative);
13
+ const variant = build.variants[mode];
14
+
15
+ return {
16
+ workflow: 'cng',
17
+ abilityName: build.identity.abilityName,
18
+ buildMode: mode,
19
+ bundleName: build.identity.bundleName,
20
+ expectedHap: resolve(variant.expectedHap),
21
+ exportPaths: Object.fromEntries(
22
+ Object.entries(build.export).map(([name, relative]) => [name, resolve(relative)])
23
+ ) as HarmonyBuildPlan['exportPaths'],
24
+ harmonyRoot: resolve(build.harmonyRoot),
25
+ hvigorArgs: [...variant.hvigorArgs],
26
+ moduleName: build.identity.moduleName,
27
+ moduleRoot: resolve(build.moduleRoot),
28
+ nativeCache: {
29
+ invalidationRoots: build.nativeCache.invalidationRoots.map(resolve),
30
+ stateFile: resolve(build.nativeCache.stateFile),
31
+ },
32
+ nativeInputs: {
33
+ lockfile: resolve(build.nativeInputs.lockfile),
34
+ manifest: resolve(build.nativeInputs.manifest),
35
+ },
36
+ productName: build.identity.productName,
37
+ projectFiles: Object.fromEntries(
38
+ Object.entries(build.projectFiles).map(([name, relative]) => [name, resolve(relative)])
39
+ ) as CngHarmonyBuildPlan['projectFiles'],
40
+ targetName: build.identity.targetName,
41
+ };
42
+ }
43
+
44
+ async function resolveHarmonyBuildPlanIfPresentAsync(
45
+ root: string,
46
+ options: { buildMode?: 'debug' | 'release' } = {}
47
+ ): Promise<HarmonyBuildPlan | null> {
48
+ const mode = options.buildMode || 'debug';
49
+ if (!['debug', 'release'].includes(mode)) {
50
+ throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', `Harmony buildMode must be debug or release, received: ${mode}`, { operation: 'resolve-build' });
51
+ }
52
+
53
+ let manifest;
54
+
55
+ try {
56
+ manifest = await readManifestIfPresentAsync(root);
57
+ } catch (cause) {
58
+ throw new HarmonyCliError(
59
+ cause.code || 'ERR_HARMONY_TEMPLATE_INVALID',
60
+ `Cannot read the generated Harmony build descriptor: ${cause.message}`,
61
+ { cause, operation: 'resolve-build' }
62
+ );
63
+ }
64
+
65
+ if (!manifest) {
66
+ return isBareHarmonyProject(root) ? resolveBareHarmonyBuildPlan(root, mode) : null;
67
+ }
68
+
69
+ return createHarmonyBuildPlan(root, manifest.build, mode);
70
+ }
71
+
72
+ async function resolveHarmonyBuildPlanAsync(
73
+ root: string,
74
+ options: { buildMode?: 'debug' | 'release' } = {}
75
+ ): Promise<HarmonyBuildPlan> {
76
+ const plan = await resolveHarmonyBuildPlanIfPresentAsync(root, options);
77
+ if (!plan) {
78
+ throw new HarmonyCliError(
79
+ 'ERR_HARMONY_MANIFEST_DRIFT',
80
+ 'Cannot read the generated Harmony build descriptor because the CNG manifest is missing.',
81
+ { operation: 'resolve-build' }
82
+ );
83
+ }
84
+
85
+ return plan;
86
+ }
87
+
88
+ export { resolveHarmonyBuildPlanAsync, resolveHarmonyBuildPlanIfPresentAsync };
@@ -1,13 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
-
4
- import {
5
- resolveHarmonyBuildPath,
6
- type HarmonyBuildDescriptor,
7
- } from '@expo-harmony/prebuild-config/build-descriptor';
8
- import { readManifestIfPresentAsync } from '@expo-harmony/prebuild-config/check';
9
-
10
- import { HarmonyCliError } from './errors';
3
+ import { resolveHarmonyCommand } from '@expo-harmony/expo-modules-autolinking/tool-command';
11
4
 
12
5
  export interface HarmonyTool {
13
6
  args: string[];
@@ -23,23 +16,6 @@ export interface HarmonyToolchain {
23
16
  toolsRoot: string | null;
24
17
  }
25
18
 
26
- export interface HarmonyBuildPlan {
27
- abilityName: string;
28
- buildMode: 'debug' | 'release';
29
- bundleName: string;
30
- expectedHap: string;
31
- exportPaths: HarmonyBuildDescriptor['export'];
32
- harmonyRoot: string;
33
- hvigorArgs: string[];
34
- moduleName: string;
35
- moduleRoot: string;
36
- nativeCache: HarmonyBuildDescriptor['nativeCache'];
37
- nativeInputs: HarmonyBuildDescriptor['nativeInputs'];
38
- projectFiles: HarmonyBuildDescriptor['projectFiles'];
39
- productName: string;
40
- targetName: string;
41
- }
42
-
43
19
  function existingFile(candidates: string[]): string | null {
44
20
  return candidates.find(candidate => fs.existsSync(candidate)) || null;
45
21
  }
@@ -54,7 +30,7 @@ const RequiredSdkComponents = Object.freeze([
54
30
  'default/openharmony/toolchains/oh-uni-package.json',
55
31
  ]);
56
32
 
57
- function sdkRootsNear(seed) {
33
+ function sdkRootsNear(seed: string): string[] {
58
34
  const roots = [];
59
35
  let cursor = path.resolve(seed);
60
36
 
@@ -74,7 +50,7 @@ function sdkRootsNear(seed) {
74
50
  return roots;
75
51
  }
76
52
 
77
- function resolveHarmonySdkRoot(env, platform) {
53
+ function resolveHarmonySdkRoot(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): string | null {
78
54
  const seeds = [
79
55
  env.DEVECO_SDK_HOME,
80
56
  env.HARMONY_HOME,
@@ -92,7 +68,7 @@ function resolveHarmonySdkRoot(env, platform) {
92
68
  )) || null;
93
69
  }
94
70
 
95
- function devEcoInstallRoots(sdkHome) {
71
+ function devEcoInstallRoots(sdkHome: string): string[] {
96
72
  const ancestors = [sdkHome, path.dirname(sdkHome), path.dirname(path.dirname(sdkHome))];
97
73
  const sdkDirectory = ancestors.find(candidate => path.basename(candidate).toLowerCase() === 'sdk');
98
74
  const roots = [
@@ -104,7 +80,7 @@ function devEcoInstallRoots(sdkHome) {
104
80
  return [...new Set(roots)];
105
81
  }
106
82
 
107
- function devEcoLayouts(sdkHome) {
83
+ function devEcoLayouts(sdkHome: string) {
108
84
  return devEcoInstallRoots(sdkHome).flatMap(root => [
109
85
  {
110
86
  nodeRoot: path.join(root, 'tools', 'node'),
@@ -117,7 +93,11 @@ function devEcoLayouts(sdkHome) {
117
93
  ]);
118
94
  }
119
95
 
120
- function devEcoNode(layout, platform, env) {
96
+ function devEcoNode(
97
+ layout: { nodeRoot: string },
98
+ platform: NodeJS.Platform,
99
+ env: NodeJS.ProcessEnv
100
+ ): Pick<HarmonyTool, 'command' | 'source'> | null {
121
101
  if (env.HARMONY_NODE) return { command: env.HARMONY_NODE, source: 'override' };
122
102
 
123
103
  const executable = platform === 'win32' ? 'node.exe' : 'node';
@@ -133,51 +113,49 @@ function resolveHarmonyToolchain(): HarmonyToolchain {
133
113
  const env = process.env;
134
114
  const platform = process.platform;
135
115
 
136
- // Hvigor expects the SDK root containing default/, not default/ itself.
137
- // Reject metadata-only candidates so doctor cannot claim an SDK is
138
- // buildable without its HMS, OpenHarmony, native, ETS, and toolchain parts.
116
+ // Hvigor needs the complete SDK root containing default/, not default/ itself.
139
117
  const sdkHome = resolveHarmonySdkRoot(env, platform);
140
118
  const layouts = sdkHome ? devEcoLayouts(sdkHome) : [];
141
119
 
142
120
  let ohpm: HarmonyTool;
143
121
  if (env.HARMONY_OHPM) {
144
- ohpm = { args: [], command: env.HARMONY_OHPM, source: 'override' };
122
+ ohpm = { ...resolveHarmonyCommand('ohpm', [], env), source: 'override' };
145
123
  } else {
146
- const devEcoOhpm = existingFile(layouts.map(layout => path.join(
124
+ const entry = layouts.map(layout => ({
125
+ node: devEcoNode(layout, platform, { ...env, HARMONY_NODE: env.HARMONY_OHPM_NODE || env.HARMONY_NODE }),
126
+ script: path.join(layout.toolsRoot, 'ohpm', 'bin', 'pm-cli.js'),
127
+ })).find(candidate => candidate.node && fs.existsSync(candidate.script));
128
+ const command = existingFile(layouts.map(layout => path.join(
147
129
  layout.toolsRoot,
148
130
  'ohpm',
149
131
  'bin',
150
132
  platform === 'win32' ? 'ohpm.bat' : 'ohpm'
151
133
  )));
152
- ohpm = devEcoOhpm
153
- ? { args: [], command: devEcoOhpm, source: 'deveco' }
154
- : { args: [], command: platform === 'win32' ? 'ohpm.bat' : 'ohpm', source: 'path' };
134
+
135
+ ohpm = entry
136
+ ? { args: [entry.script], command: entry.node.command, source: 'deveco' }
137
+ : command
138
+ ? { args: [], command, source: 'deveco' }
139
+ : { args: [], command: platform === 'win32' ? 'ohpm.bat' : 'ohpm', source: 'path' };
155
140
  }
156
141
 
157
142
  let hvigor: HarmonyTool;
158
143
  if (env.HARMONY_HVIGORW) {
159
- if (/\.(?:c|m)?js$/iu.test(env.HARMONY_HVIGORW)) {
160
- hvigor = {
161
- args: [env.HARMONY_HVIGORW],
162
- command: env.HARMONY_NODE || process.execPath,
163
- source: 'override',
164
- };
165
- } else {
166
- hvigor = { args: [], command: env.HARMONY_HVIGORW, source: 'override' };
167
- }
144
+ hvigor = { ...resolveHarmonyCommand('hvigorw', [], env), source: 'override' };
168
145
  } else {
169
- const devEcoHvigor = layouts.map(layout => ({
170
- layout,
171
- node: devEcoNode(layout, platform, env),
146
+ const entry = layouts.map(layout => ({
147
+ node: devEcoNode(layout, platform, { ...env, HARMONY_NODE: env.HARMONY_HVIGOR_NODE || env.HARMONY_NODE }),
172
148
  script: path.join(layout.toolsRoot, 'hvigor', 'bin', 'hvigorw.js'),
173
149
  })).find(candidate => candidate.node && fs.existsSync(candidate.script));
174
- hvigor = devEcoHvigor
175
- ? { args: [devEcoHvigor.script], command: devEcoHvigor.node.command, source: 'deveco' }
150
+
151
+ hvigor = entry
152
+ ? { args: [entry.script], command: entry.node.command, source: 'deveco' }
176
153
  : { args: [], command: platform === 'win32' ? 'hvigorw.bat' : 'hvigorw', source: 'path' };
177
154
  }
178
155
 
179
156
  const toolsRoot = layouts.find(layout => (
180
157
  hvigor.args[0]?.startsWith(`${layout.toolsRoot}${path.sep}`)
158
+ || ohpm.args[0]?.startsWith(`${layout.toolsRoot}${path.sep}`)
181
159
  || ohpm.command.startsWith(`${layout.toolsRoot}${path.sep}`)
182
160
  ))?.toolsRoot || null;
183
161
 
@@ -201,10 +179,14 @@ function resolveHarmonyToolchain(): HarmonyToolchain {
201
179
  function createHarmonyToolchainEnv(toolchain = resolveHarmonyToolchain()): NodeJS.ProcessEnv {
202
180
  return {
203
181
  ...process.env,
204
- HARMONY_OHPM: toolchain.ohpm.command,
205
- // A script-based Hvigor invocation must retain both its script and Node executable.
182
+ HARMONY_OHPM: toolchain.ohpm.args[0] || toolchain.ohpm.command,
183
+ HARMONY_OHPM_NODE: toolchain.ohpm.args.length > 0 ? toolchain.ohpm.command : undefined,
184
+ // Script-based tools must retain both their script and Node executable.
206
185
  HARMONY_HVIGORW: toolchain.hvigor.args[0] || toolchain.hvigor.command,
207
- ...(toolchain.hvigor.args.length > 0 ? { HARMONY_NODE: toolchain.hvigor.command } : {}),
186
+ HARMONY_HVIGOR_NODE: toolchain.hvigor.args.length > 0 ? toolchain.hvigor.command : undefined,
187
+ ...(toolchain.hvigor.args.length > 0
188
+ ? { HARMONY_NODE: toolchain.hvigor.command }
189
+ : toolchain.ohpm.args.length > 0 ? { HARMONY_NODE: toolchain.ohpm.command } : {}),
208
190
  ...(toolchain.sdkHome && !process.env.DEVECO_SDK_HOME
209
191
  ? { DEVECO_SDK_HOME: toolchain.sdkHome }
210
192
  : {}),
@@ -229,88 +211,4 @@ function resolveHarmonyEmulator(toolchain: HarmonyToolchain): HarmonyTool {
229
211
  : { args: [], command: executable, source: 'path' };
230
212
  }
231
213
 
232
- function createHarmonyBuildPlan(
233
- projectRoot: string,
234
- build: HarmonyBuildDescriptor,
235
- mode: 'debug' | 'release'
236
- ): HarmonyBuildPlan {
237
- const resolve = relative => resolveHarmonyBuildPath(projectRoot, relative);
238
- const variant = build.variants[mode];
239
-
240
- return {
241
- abilityName: build.identity.abilityName,
242
- buildMode: mode,
243
- bundleName: build.identity.bundleName,
244
- expectedHap: resolve(variant.expectedHap),
245
- exportPaths: Object.fromEntries(
246
- Object.entries(build.export).map(([name, relative]) => [name, resolve(relative)])
247
- ) as HarmonyBuildPlan['exportPaths'],
248
- harmonyRoot: resolve(build.harmonyRoot),
249
- hvigorArgs: [...variant.hvigorArgs],
250
- moduleName: build.identity.moduleName,
251
- moduleRoot: resolve(build.moduleRoot),
252
- nativeCache: {
253
- invalidationRoots: build.nativeCache.invalidationRoots.map(resolve),
254
- stateFile: resolve(build.nativeCache.stateFile),
255
- },
256
- nativeInputs: {
257
- lockfile: resolve(build.nativeInputs.lockfile),
258
- manifest: resolve(build.nativeInputs.manifest),
259
- },
260
- productName: build.identity.productName,
261
- projectFiles: Object.fromEntries(
262
- Object.entries(build.projectFiles).map(([name, relative]) => [name, resolve(relative)])
263
- ) as HarmonyBuildPlan['projectFiles'],
264
- targetName: build.identity.targetName,
265
- };
266
- }
267
-
268
- async function resolveHarmonyBuildPlanIfPresentAsync(
269
- projectRoot: string,
270
- options: { buildMode?: 'debug' | 'release' } = {}
271
- ): Promise<HarmonyBuildPlan | null> {
272
- const mode = options.buildMode || 'debug';
273
- if (!['debug', 'release'].includes(mode)) {
274
- throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', `Harmony buildMode must be debug or release, received: ${mode}`, { operation: 'resolve-build' });
275
- }
276
-
277
- let manifest;
278
-
279
- try {
280
- manifest = await readManifestIfPresentAsync(projectRoot);
281
- } catch (cause) {
282
- throw new HarmonyCliError(
283
- cause.code || 'ERR_HARMONY_TEMPLATE_INVALID',
284
- `Cannot read the generated Harmony build descriptor: ${cause.message}`,
285
- { cause, operation: 'resolve-build' }
286
- );
287
- }
288
-
289
- if (!manifest) return null;
290
-
291
- return createHarmonyBuildPlan(projectRoot, manifest.build, mode);
292
- }
293
-
294
- async function resolveHarmonyBuildPlanAsync(
295
- projectRoot: string,
296
- options: { buildMode?: 'debug' | 'release' } = {}
297
- ): Promise<HarmonyBuildPlan> {
298
- const plan = await resolveHarmonyBuildPlanIfPresentAsync(projectRoot, options);
299
- if (!plan) {
300
- throw new HarmonyCliError(
301
- 'ERR_HARMONY_MANIFEST_DRIFT',
302
- 'Cannot read the generated Harmony build descriptor because the CNG manifest is missing.',
303
- { operation: 'resolve-build' }
304
- );
305
- }
306
-
307
- return plan;
308
- }
309
-
310
- export {
311
- createHarmonyToolchainEnv,
312
- resolveHarmonyBuildPlanAsync,
313
- resolveHarmonyBuildPlanIfPresentAsync,
314
- resolveHarmonyEmulator,
315
- resolveHarmonyToolchain,
316
- };
214
+ export { createHarmonyToolchainEnv, resolveHarmonyEmulator, resolveHarmonyToolchain };
@@ -0,0 +1,44 @@
1
+ export type HarmonyBuildMode = 'debug' | 'release';
2
+
3
+ interface NativeBuildPlan {
4
+ abilityName: string;
5
+ buildMode: HarmonyBuildMode;
6
+ bundleName: string;
7
+ expectedHap: string;
8
+ exportPaths: {
9
+ bundle: string;
10
+ manifest: string;
11
+ metadataRoot: string;
12
+ rawfileRoot: string;
13
+ sourceMap: string;
14
+ };
15
+ harmonyRoot: string;
16
+ hvigorArgs: string[];
17
+ moduleName: string;
18
+ moduleRoot: string;
19
+ nativeCache: { invalidationRoots: string[]; stateFile: string };
20
+ nativeInputs: { lockfile: string; manifest: string };
21
+ projectFiles: {
22
+ hvigorConfig: string;
23
+ moduleHvigor: string;
24
+ moduleJson: string;
25
+ projectBuildProfile: string;
26
+ rootHvigor: string;
27
+ };
28
+ productName: string;
29
+ targetName: string;
30
+ }
31
+
32
+ export interface BareHarmonyBuildPlan extends NativeBuildPlan {
33
+ workflow: 'bare';
34
+ }
35
+
36
+ export interface CngHarmonyBuildPlan extends NativeBuildPlan {
37
+ workflow: 'cng';
38
+ projectFiles: NativeBuildPlan['projectFiles'] & {
39
+ nativeInputsStamp: string;
40
+ templateMarker: string;
41
+ };
42
+ }
43
+
44
+ export type HarmonyBuildPlan = BareHarmonyBuildPlan | CngHarmonyBuildPlan;
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import fs from 'node:fs';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
@@ -5,20 +6,22 @@ import path from 'node:path';
5
6
  import {
6
7
  compareAsync,
7
8
  stageAsync,
8
- } from '@expo-harmony/prebuild-config/check';
9
+ } from '@expo-harmony/prebuild-config/internal';
9
10
 
10
11
  import { HarmonyCliError } from '../errors';
11
12
  import { isInside } from '../path';
12
13
  import { spawnAsync } from '../process';
13
14
  import { withHarmonyProjectLockAsync } from '../projectLock';
14
- import { createHarmonyToolchainEnv, resolveHarmonyBuildPlanAsync, type HarmonyBuildPlan } from '../tools';
15
+ import { createHarmonyToolchainEnv } from '../native/toolchain';
16
+ import { resolveHarmonyBuildPlanAsync } from '../native/project';
17
+ import { type HarmonyBuildPlan } from '../native/types';
15
18
  import { resolveExpoCli } from '../expo';
16
19
  import { packAsync } from './template';
17
20
 
18
- export type { Change as CheckChange } from '@expo-harmony/prebuild-config/check';
21
+ export type { Change as CheckChange } from '@expo-harmony/prebuild-config/internal';
19
22
 
20
- const IgnoredProjectDirectories = new Set(['.expo', '.git', '.hvigor', '.yarn', 'node_modules']);
21
- const IgnoredHarmonyGeneratedDirectories = new Set([
23
+ const IgnoredDirectories = new Set(['.expo', '.git', '.hvigor', '.yarn', 'node_modules']);
24
+ const GeneratedDirectories = new Set([
22
25
  '.cxx',
23
26
  '.git',
24
27
  '.hvigor',
@@ -40,7 +43,28 @@ function mirrorRoot(temp, project) {
40
43
  return path.join(temp, 'filesystem', volume, ...segments);
41
44
  }
42
45
 
43
- async function linkModulesAsync(source, target) {
46
+ async function linkModuleEntryAsync(
47
+ source: string,
48
+ target: string,
49
+ entry: fs.Dirent
50
+ ) {
51
+ const directory = entry.isSymbolicLink()
52
+ ? (await fs.promises.stat(source)).isDirectory()
53
+ : entry.isDirectory();
54
+
55
+ if (process.platform === 'win32' && !directory) {
56
+ await fs.promises.copyFile(source, target);
57
+ return;
58
+ }
59
+
60
+ await fs.promises.symlink(
61
+ path.resolve(source),
62
+ target,
63
+ directory ? process.platform === 'win32' ? 'junction' : 'dir' : 'file'
64
+ );
65
+ }
66
+
67
+ async function linkModulesAsync(source: string, target: string) {
44
68
  await fs.promises.mkdir(target, { recursive: true });
45
69
 
46
70
  for (const entry of await fs.promises.readdir(source, { withFileTypes: true })) {
@@ -49,25 +73,18 @@ async function linkModulesAsync(source, target) {
49
73
 
50
74
  if (entry.name.startsWith('@') && entry.isDirectory() && !entry.isSymbolicLink()) {
51
75
  await fs.promises.mkdir(to);
76
+
52
77
  for (const child of await fs.promises.readdir(from, { withFileTypes: true })) {
53
- await fs.promises.symlink(
78
+ await linkModuleEntryAsync(
54
79
  path.join(from, child.name),
55
80
  path.join(to, child.name),
56
- process.platform === 'win32' && (child.isDirectory() || child.isSymbolicLink())
57
- ? 'junction'
58
- : child.isDirectory() ? 'dir' : 'file'
81
+ child
59
82
  );
60
83
  }
61
84
  continue;
62
85
  }
63
86
 
64
- await fs.promises.symlink(
65
- from,
66
- to,
67
- process.platform === 'win32' && (entry.isDirectory() || entry.isSymbolicLink())
68
- ? 'junction'
69
- : entry.isDirectory() ? 'dir' : 'file'
70
- );
87
+ await linkModuleEntryAsync(from, to, entry);
71
88
  }
72
89
  }
73
90
 
@@ -89,16 +106,18 @@ function shouldCopyPrebuildCheckPath(
89
106
  if (!relative) return true;
90
107
 
91
108
  const segments = relative.split(path.sep);
92
- if (IgnoredProjectDirectories.has(segments[0])) return false;
109
+ if (IgnoredDirectories.has(segments[0])) return false;
93
110
 
94
- const nativeRoot = isInside(plan.harmonyRoot, source)
111
+ const root = isInside(plan.harmonyRoot, source)
95
112
  ? plan.harmonyRoot
96
113
  : isAppLocalHarmonyPath(project, source)
97
114
  ? path.join(project, ...segments.slice(0, 3))
98
115
  : null;
99
- if (nativeRoot) {
100
- const native = path.relative(nativeRoot, source).split(path.sep);
101
- if (native.some(segment => IgnoredHarmonyGeneratedDirectories.has(segment))) return false;
116
+ if (root) {
117
+ const native = path.relative(root, source).split(path.sep);
118
+ if (GeneratedDirectories.has(native[0])
119
+ || (GeneratedDirectories.has(native[1])
120
+ && fs.existsSync(path.join(root, native[0], 'src/main/module.json5')))) return false;
102
121
  }
103
122
 
104
123
  return source !== plan.exportPaths.bundle;
@@ -112,6 +131,7 @@ async function copyAsync(
112
131
  ) {
113
132
  await fs.promises.cp(project, target, {
114
133
  recursive: true,
134
+ verbatimSymlinks: true,
115
135
  filter: source => shouldCopyPrebuildCheckPath(project, source, plan),
116
136
  });
117
137
 
@@ -123,16 +143,16 @@ async function copyAsync(
123
143
  });
124
144
  }
125
145
 
126
- // Keep a real node_modules directory in the isolated project and link its
127
- // entries. Dependency scanners then retain the isolated lexical package path
128
- // (including scoped packages) instead of collapsing the entire node_modules
129
- // root to the source project's realpath. The packages remain read-only and
130
- // are never copied or modified by --check.
146
+ // Link individual packages so dependency scanners retain paths inside the mirror.
131
147
  await linkModulesAsync(modules, path.join(target, 'node_modules'));
132
148
  await stageAsync(project, target, temp);
133
149
  }
134
150
 
135
- async function checkUnlockedAsync(project, options: CheckOptions) {
151
+ async function withGeneratedProjectAsync<T>(
152
+ project: string,
153
+ options: CheckOptions & { clean?: boolean; skipPatches?: boolean },
154
+ action: (expected: string, checksum: string) => Promise<T>
155
+ ): Promise<T> {
136
156
  project = path.resolve(project);
137
157
  const plan = await resolveHarmonyBuildPlanAsync(project);
138
158
  const temp = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'expo-harmony-check-'));
@@ -141,6 +161,8 @@ async function checkUnlockedAsync(project, options: CheckOptions) {
141
161
 
142
162
  try {
143
163
  await copyAsync(project, expected, plan, temp);
164
+ if (options.clean) await fs.promises.rm(path.join(expected, 'harmony'), { recursive: true, force: true });
165
+
144
166
  packed = await packAsync(project);
145
167
 
146
168
  const expo = resolveExpoCli(project);
@@ -159,6 +181,7 @@ async function checkUnlockedAsync(project, options: CheckOptions) {
159
181
  ...packed.env,
160
182
  ...(options.buildType ? { EXPO_HARMONY_BUILD_TYPE: options.buildType } : {}),
161
183
  EXPO_HARMONY_CHECK_MIRROR_ROOT: temp,
184
+ ...(options.skipPatches ? { EXPO_HARMONY_SKIP_PATCHES: '1' } : {}),
162
185
  },
163
186
  operation: 'check-prebuild',
164
187
  });
@@ -171,7 +194,9 @@ async function checkUnlockedAsync(project, options: CheckOptions) {
171
194
  );
172
195
  }
173
196
 
174
- return compareAsync(project, expected);
197
+ const checksum = createHash('md5').update(Uint8Array.from(await fs.promises.readFile(packed.tarball))).digest('hex');
198
+
199
+ return await action(expected, checksum);
175
200
  } finally {
176
201
  if (packed) await packed.cleanup();
177
202
  await fs.promises.rm(temp, { recursive: true, force: true });
@@ -182,8 +207,8 @@ async function checkAsync(project, options: CheckOptions = {}) {
182
207
  return withHarmonyProjectLockAsync(
183
208
  project,
184
209
  'prebuild-check',
185
- () => checkUnlockedAsync(project, options)
210
+ () => withGeneratedProjectAsync(project, options, expected => compareAsync(project, expected))
186
211
  );
187
212
  }
188
213
 
189
- export { checkAsync };
214
+ export { checkAsync, withGeneratedProjectAsync };