@expo-harmony/cli 55.0.26-harmony.7 → 55.0.26-harmony.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +74 -46
  2. package/build/bin/expo-harmony.js +1 -1
  3. package/build/buildHap/build.d.ts +3 -10
  4. package/build/buildHap/build.js +11 -69
  5. package/build/buildHap/index.d.ts +2 -0
  6. package/build/buildHap/index.js +19 -0
  7. package/build/cli.d.ts +3 -3
  8. package/build/cli.js +21 -187
  9. package/build/command.d.ts +9 -0
  10. package/build/command.js +20 -0
  11. package/build/doctor/doctor.d.ts +1 -1
  12. package/build/doctor/doctor.js +64 -56
  13. package/build/doctor/index.d.ts +2 -0
  14. package/build/doctor/index.js +16 -0
  15. package/build/exportEmbed/export.d.ts +2 -2
  16. package/build/exportEmbed/export.js +53 -49
  17. package/build/exportEmbed/index.d.ts +2 -0
  18. package/build/exportEmbed/index.js +19 -0
  19. package/build/file.d.ts +2 -1
  20. package/build/file.js +18 -8
  21. package/build/help.d.ts +1 -0
  22. package/build/help.js +36 -0
  23. package/build/index.d.ts +5 -3
  24. package/build/index.js +6 -5
  25. package/build/internal/prebuild.d.ts +3 -0
  26. package/build/internal/prebuild.js +9 -0
  27. package/build/log.d.ts +4 -0
  28. package/build/log.js +6 -0
  29. package/build/modules/index.d.ts +2 -0
  30. package/build/modules/index.js +16 -0
  31. package/build/native/bare.d.ts +4 -0
  32. package/build/native/bare.js +130 -0
  33. package/build/native/build.d.ts +15 -0
  34. package/build/native/build.js +67 -0
  35. package/build/{run → native}/cache.d.ts +2 -2
  36. package/build/{run → native}/cache.js +8 -8
  37. package/build/native/hvigor.d.ts +2 -0
  38. package/build/native/hvigor.js +77 -0
  39. package/build/native/install.d.ts +5 -0
  40. package/build/native/install.js +13 -0
  41. package/build/native/prepareProject.d.ts +11 -0
  42. package/build/native/prepareProject.js +68 -0
  43. package/build/native/project.d.ts +8 -0
  44. package/build/native/project.js +58 -0
  45. package/build/native/toolchain.d.ts +16 -0
  46. package/build/{tools.js → native/toolchain.js} +1 -57
  47. package/build/native/types.d.ts +47 -0
  48. package/build/native/types.js +2 -0
  49. package/build/prebuild/check.d.ts +7 -3
  50. package/build/prebuild/check.js +26 -20
  51. package/build/prebuild/clean.js +5 -5
  52. package/build/prebuild/index.d.ts +2 -0
  53. package/build/prebuild/index.js +51 -0
  54. package/build/prebuild/prebuild.js +11 -3
  55. package/build/prebuild/template.js +2 -2
  56. package/build/process.d.ts +11 -1
  57. package/build/process.js +16 -0
  58. package/build/profile.d.ts +1 -0
  59. package/build/profile.js +12 -0
  60. package/build/projectLock.js +7 -2
  61. package/build/run/devices.d.ts +1 -1
  62. package/build/run/devices.js +15 -10
  63. package/build/run/emulators.d.ts +1 -1
  64. package/build/run/emulators.js +2 -5
  65. package/build/run/index.d.ts +2 -0
  66. package/build/run/index.js +25 -0
  67. package/build/run/metro.js +1 -1
  68. package/build/run/run.d.ts +4 -10
  69. package/build/run/run.js +45 -90
  70. package/build/start/index.d.ts +2 -0
  71. package/build/start/index.js +33 -0
  72. package/package.json +14 -4
  73. package/src/bin/expo-harmony.ts +2 -2
  74. package/src/buildHap/build.ts +13 -105
  75. package/src/buildHap/index.ts +19 -0
  76. package/src/cli.ts +24 -227
  77. package/src/command.ts +24 -0
  78. package/src/doctor/doctor.ts +63 -61
  79. package/src/doctor/index.ts +16 -0
  80. package/src/exportEmbed/export.ts +64 -55
  81. package/src/exportEmbed/index.ts +17 -0
  82. package/src/file.ts +20 -11
  83. package/src/help.ts +33 -0
  84. package/src/index.ts +5 -3
  85. package/src/internal/prebuild.ts +3 -0
  86. package/src/log.ts +5 -0
  87. package/src/modules/index.ts +16 -0
  88. package/src/native/bare.ts +145 -0
  89. package/src/native/build.ts +81 -0
  90. package/src/{run → native}/cache.ts +10 -9
  91. package/src/native/hvigor.ts +81 -0
  92. package/src/native/install.ts +17 -0
  93. package/src/native/prepareProject.ts +99 -0
  94. package/src/native/project.ts +88 -0
  95. package/src/{tools.ts → native/toolchain.ts} +11 -118
  96. package/src/native/types.ts +44 -0
  97. package/src/prebuild/check.ts +30 -19
  98. package/src/prebuild/clean.ts +3 -3
  99. package/src/prebuild/index.ts +56 -0
  100. package/src/prebuild/prebuild.ts +11 -1
  101. package/src/prebuild/template.ts +2 -2
  102. package/src/process.ts +38 -1
  103. package/src/profile.ts +13 -0
  104. package/src/projectLock.ts +9 -1
  105. package/src/run/devices.ts +16 -11
  106. package/src/run/emulators.ts +4 -6
  107. package/src/run/index.ts +25 -0
  108. package/src/run/metro.ts +1 -1
  109. package/src/run/run.ts +56 -129
  110. package/src/start/index.ts +31 -0
  111. package/build/buildHap/common.d.ts +0 -24
  112. package/build/buildHap/common.js +0 -92
  113. package/build/run/install.d.ts +0 -7
  114. package/build/run/install.js +0 -26
  115. package/build/tools.d.ts +0 -39
  116. package/src/buildHap/common.ts +0 -145
  117. package/src/run/install.ts +0 -41
@@ -4,16 +4,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createHarmonyToolchainEnv = createHarmonyToolchainEnv;
7
- exports.resolveHarmonyBuildPlanAsync = resolveHarmonyBuildPlanAsync;
8
- exports.resolveHarmonyBuildPlanIfPresentAsync = resolveHarmonyBuildPlanIfPresentAsync;
9
7
  exports.resolveHarmonyEmulator = resolveHarmonyEmulator;
10
8
  exports.resolveHarmonyToolchain = resolveHarmonyToolchain;
11
9
  const node_fs_1 = __importDefault(require("node:fs"));
12
10
  const node_path_1 = __importDefault(require("node:path"));
13
11
  const tool_command_1 = require("@expo-harmony/expo-modules-autolinking/tool-command");
14
- const build_descriptor_1 = require("@expo-harmony/prebuild-config/build-descriptor");
15
- const check_1 = require("@expo-harmony/prebuild-config/check");
16
- const errors_1 = require("./errors");
17
12
  function existingFile(candidates) {
18
13
  return candidates.find(candidate => node_fs_1.default.existsSync(candidate)) || null;
19
14
  }
@@ -93,9 +88,7 @@ function devEcoNode(layout, platform, env) {
93
88
  function resolveHarmonyToolchain() {
94
89
  const env = process.env;
95
90
  const platform = process.platform;
96
- // Hvigor expects the SDK root containing default/, not default/ itself.
97
- // Reject metadata-only candidates so doctor cannot claim an SDK is
98
- // buildable without its HMS, OpenHarmony, native, ETS, and toolchain parts.
91
+ // Hvigor needs the complete SDK root containing default/, not default/ itself.
99
92
  const sdkHome = resolveHarmonySdkRoot(env, platform);
100
93
  const layouts = sdkHome ? devEcoLayouts(sdkHome) : [];
101
94
  let ohpm;
@@ -177,52 +170,3 @@ function resolveHarmonyEmulator(toolchain) {
177
170
  ? { args: [], command, source: 'deveco' }
178
171
  : { args: [], command: executable, source: 'path' };
179
172
  }
180
- function createHarmonyBuildPlan(projectRoot, build, mode) {
181
- const resolve = relative => (0, build_descriptor_1.resolveHarmonyBuildPath)(projectRoot, relative);
182
- const variant = build.variants[mode];
183
- return {
184
- abilityName: build.identity.abilityName,
185
- buildMode: mode,
186
- bundleName: build.identity.bundleName,
187
- expectedHap: resolve(variant.expectedHap),
188
- exportPaths: Object.fromEntries(Object.entries(build.export).map(([name, relative]) => [name, resolve(relative)])),
189
- harmonyRoot: resolve(build.harmonyRoot),
190
- hvigorArgs: [...variant.hvigorArgs],
191
- moduleName: build.identity.moduleName,
192
- moduleRoot: resolve(build.moduleRoot),
193
- nativeCache: {
194
- invalidationRoots: build.nativeCache.invalidationRoots.map(resolve),
195
- stateFile: resolve(build.nativeCache.stateFile),
196
- },
197
- nativeInputs: {
198
- lockfile: resolve(build.nativeInputs.lockfile),
199
- manifest: resolve(build.nativeInputs.manifest),
200
- },
201
- productName: build.identity.productName,
202
- projectFiles: Object.fromEntries(Object.entries(build.projectFiles).map(([name, relative]) => [name, resolve(relative)])),
203
- targetName: build.identity.targetName,
204
- };
205
- }
206
- async function resolveHarmonyBuildPlanIfPresentAsync(projectRoot, options = {}) {
207
- const mode = options.buildMode || 'debug';
208
- if (!['debug', 'release'].includes(mode)) {
209
- throw new errors_1.HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', `Harmony buildMode must be debug or release, received: ${mode}`, { operation: 'resolve-build' });
210
- }
211
- let manifest;
212
- try {
213
- manifest = await (0, check_1.readManifestIfPresentAsync)(projectRoot);
214
- }
215
- catch (cause) {
216
- throw new errors_1.HarmonyCliError(cause.code || 'ERR_HARMONY_TEMPLATE_INVALID', `Cannot read the generated Harmony build descriptor: ${cause.message}`, { cause, operation: 'resolve-build' });
217
- }
218
- if (!manifest)
219
- return null;
220
- return createHarmonyBuildPlan(projectRoot, manifest.build, mode);
221
- }
222
- async function resolveHarmonyBuildPlanAsync(projectRoot, options = {}) {
223
- const plan = await resolveHarmonyBuildPlanIfPresentAsync(projectRoot, options);
224
- if (!plan) {
225
- throw new errors_1.HarmonyCliError('ERR_HARMONY_MANIFEST_DRIFT', 'Cannot read the generated Harmony build descriptor because the CNG manifest is missing.', { operation: 'resolve-build' });
226
- }
227
- return plan;
228
- }
@@ -0,0 +1,47 @@
1
+ export type HarmonyBuildMode = 'debug' | 'release';
2
+ interface NativeBuildPlan {
3
+ abilityName: string;
4
+ buildMode: HarmonyBuildMode;
5
+ bundleName: string;
6
+ expectedHap: string;
7
+ exportPaths: {
8
+ bundle: string;
9
+ manifest: string;
10
+ metadataRoot: string;
11
+ rawfileRoot: string;
12
+ sourceMap: string;
13
+ };
14
+ harmonyRoot: string;
15
+ hvigorArgs: string[];
16
+ moduleName: string;
17
+ moduleRoot: string;
18
+ nativeCache: {
19
+ invalidationRoots: string[];
20
+ stateFile: string;
21
+ };
22
+ nativeInputs: {
23
+ lockfile: string;
24
+ manifest: string;
25
+ };
26
+ projectFiles: {
27
+ hvigorConfig: string;
28
+ moduleHvigor: string;
29
+ moduleJson: string;
30
+ projectBuildProfile: string;
31
+ rootHvigor: string;
32
+ };
33
+ productName: string;
34
+ targetName: string;
35
+ }
36
+ export interface BareHarmonyBuildPlan extends NativeBuildPlan {
37
+ workflow: 'bare';
38
+ }
39
+ export interface CngHarmonyBuildPlan extends NativeBuildPlan {
40
+ workflow: 'cng';
41
+ projectFiles: NativeBuildPlan['projectFiles'] & {
42
+ nativeInputsStamp: string;
43
+ templateMarker: string;
44
+ };
45
+ }
46
+ export type HarmonyBuildPlan = BareHarmonyBuildPlan | CngHarmonyBuildPlan;
47
+ export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,6 +1,10 @@
1
- export type { Change as CheckChange } from '@expo-harmony/prebuild-config/check';
1
+ export type { Change as CheckChange } from '@expo-harmony/prebuild-config/internal';
2
2
  interface CheckOptions {
3
3
  buildType?: 'debug' | 'release';
4
4
  }
5
- declare function checkAsync(project: any, options?: CheckOptions): Promise<import("@expo-harmony/prebuild-config/check").Result>;
6
- export { checkAsync };
5
+ declare function withGeneratedProjectAsync<T>(project: string, options: CheckOptions & {
6
+ clean?: boolean;
7
+ skipPatches?: boolean;
8
+ }, action: (expected: string, checksum: string) => Promise<T>): Promise<T>;
9
+ declare function checkAsync(project: any, options?: CheckOptions): Promise<import("@expo-harmony/prebuild-config/internal").Result>;
10
+ export { checkAsync, withGeneratedProjectAsync };
@@ -4,19 +4,22 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.checkAsync = checkAsync;
7
+ exports.withGeneratedProjectAsync = withGeneratedProjectAsync;
8
+ const node_crypto_1 = require("node:crypto");
7
9
  const node_fs_1 = __importDefault(require("node:fs"));
8
10
  const node_os_1 = __importDefault(require("node:os"));
9
11
  const node_path_1 = __importDefault(require("node:path"));
10
- const check_1 = require("@expo-harmony/prebuild-config/check");
12
+ const internal_1 = require("@expo-harmony/prebuild-config/internal");
11
13
  const errors_1 = require("../errors");
12
14
  const path_1 = require("../path");
13
15
  const process_1 = require("../process");
14
16
  const projectLock_1 = require("../projectLock");
15
- const tools_1 = require("../tools");
17
+ const toolchain_1 = require("../native/toolchain");
18
+ const project_1 = require("../native/project");
16
19
  const expo_1 = require("../expo");
17
20
  const template_1 = require("./template");
18
- const IgnoredProjectDirectories = new Set(['.expo', '.git', '.hvigor', '.yarn', 'node_modules']);
19
- const IgnoredHarmonyGeneratedDirectories = new Set([
21
+ const IgnoredDirectories = new Set(['.expo', '.git', '.hvigor', '.yarn', 'node_modules']);
22
+ const GeneratedDirectories = new Set([
20
23
  '.cxx',
21
24
  '.git',
22
25
  '.hvigor',
@@ -68,16 +71,18 @@ function shouldCopyPrebuildCheckPath(project, source, plan) {
68
71
  if (!relative)
69
72
  return true;
70
73
  const segments = relative.split(node_path_1.default.sep);
71
- if (IgnoredProjectDirectories.has(segments[0]))
74
+ if (IgnoredDirectories.has(segments[0]))
72
75
  return false;
73
- const nativeRoot = (0, path_1.isInside)(plan.harmonyRoot, source)
76
+ const root = (0, path_1.isInside)(plan.harmonyRoot, source)
74
77
  ? plan.harmonyRoot
75
78
  : isAppLocalHarmonyPath(project, source)
76
79
  ? node_path_1.default.join(project, ...segments.slice(0, 3))
77
80
  : null;
78
- if (nativeRoot) {
79
- const native = node_path_1.default.relative(nativeRoot, source).split(node_path_1.default.sep);
80
- if (native.some(segment => IgnoredHarmonyGeneratedDirectories.has(segment)))
81
+ if (root) {
82
+ const native = node_path_1.default.relative(root, source).split(node_path_1.default.sep);
83
+ if (GeneratedDirectories.has(native[0])
84
+ || (GeneratedDirectories.has(native[1])
85
+ && node_fs_1.default.existsSync(node_path_1.default.join(root, native[0], 'src/main/module.json5'))))
81
86
  return false;
82
87
  }
83
88
  return source !== plan.exportPaths.bundle;
@@ -85,6 +90,7 @@ function shouldCopyPrebuildCheckPath(project, source, plan) {
85
90
  async function copyAsync(project, target, plan, temp = node_path_1.default.dirname(target)) {
86
91
  await node_fs_1.default.promises.cp(project, target, {
87
92
  recursive: true,
93
+ verbatimSymlinks: true,
88
94
  filter: source => shouldCopyPrebuildCheckPath(project, source, plan),
89
95
  });
90
96
  const modules = node_path_1.default.join(project, 'node_modules');
@@ -93,22 +99,20 @@ async function copyAsync(project, target, plan, temp = node_path_1.default.dirna
93
99
  operation: 'check',
94
100
  });
95
101
  }
96
- // Keep a real node_modules directory in the isolated project and link its
97
- // entries. Dependency scanners then retain the isolated lexical package path
98
- // (including scoped packages) instead of collapsing the entire node_modules
99
- // root to the source project's realpath. Package directories stay linked;
100
- // Windows copies loose files to avoid requiring file-symlink privileges.
102
+ // Link individual packages so dependency scanners retain paths inside the mirror.
101
103
  await linkModulesAsync(modules, node_path_1.default.join(target, 'node_modules'));
102
- await (0, check_1.stageAsync)(project, target, temp);
104
+ await (0, internal_1.stageAsync)(project, target, temp);
103
105
  }
104
- async function checkUnlockedAsync(project, options) {
106
+ async function withGeneratedProjectAsync(project, options, action) {
105
107
  project = node_path_1.default.resolve(project);
106
- const plan = await (0, tools_1.resolveHarmonyBuildPlanAsync)(project);
108
+ const plan = await (0, project_1.resolveHarmonyBuildPlanAsync)(project);
107
109
  const temp = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'expo-harmony-check-'));
108
110
  const expected = mirrorRoot(temp, project);
109
111
  let packed;
110
112
  try {
111
113
  await copyAsync(project, expected, plan, temp);
114
+ if (options.clean)
115
+ await node_fs_1.default.promises.rm(node_path_1.default.join(expected, 'harmony'), { recursive: true, force: true });
112
116
  packed = await (0, template_1.packAsync)(project);
113
117
  const expo = (0, expo_1.resolveExpoCli)(project);
114
118
  const result = await (0, process_1.spawnAsync)(process.execPath, [
@@ -122,17 +126,19 @@ async function checkUnlockedAsync(project, options) {
122
126
  capture: true,
123
127
  cwd: expected,
124
128
  env: {
125
- ...(0, tools_1.createHarmonyToolchainEnv)(),
129
+ ...(0, toolchain_1.createHarmonyToolchainEnv)(),
126
130
  ...packed.env,
127
131
  ...(options.buildType ? { EXPO_HARMONY_BUILD_TYPE: options.buildType } : {}),
128
132
  EXPO_HARMONY_CHECK_MIRROR_ROOT: temp,
133
+ ...(options.skipPatches ? { EXPO_HARMONY_SKIP_PATCHES: '1' } : {}),
129
134
  },
130
135
  operation: 'check-prebuild',
131
136
  });
132
137
  if (result.code !== 0) {
133
138
  throw new errors_1.HarmonyCliError('ERR_HARMONY_MANIFEST_DRIFT', `Isolated Expo prebuild failed:\n${result.stderr || result.stdout}`, { exitCode: result.code, operation: 'check-prebuild' });
134
139
  }
135
- return (0, check_1.compareAsync)(project, expected);
140
+ const checksum = (0, node_crypto_1.createHash)('md5').update(Uint8Array.from(await node_fs_1.default.promises.readFile(packed.tarball))).digest('hex');
141
+ return await action(expected, checksum);
136
142
  }
137
143
  finally {
138
144
  if (packed)
@@ -141,5 +147,5 @@ async function checkUnlockedAsync(project, options) {
141
147
  }
142
148
  }
143
149
  async function checkAsync(project, options = {}) {
144
- return (0, projectLock_1.withHarmonyProjectLockAsync)(project, 'prebuild-check', () => checkUnlockedAsync(project, options));
150
+ return (0, projectLock_1.withHarmonyProjectLockAsync)(project, 'prebuild-check', () => withGeneratedProjectAsync(project, options, expected => (0, internal_1.compareAsync)(project, expected)));
145
151
  }
@@ -6,14 +6,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.assertSafeCleanTarget = assertSafeCleanTarget;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
- const build_descriptor_1 = require("@expo-harmony/prebuild-config/build-descriptor");
9
+ const internal_1 = require("@expo-harmony/prebuild-config/internal");
10
10
  const errors_1 = require("../errors");
11
11
  const path_1 = require("../path");
12
- const tools_1 = require("../tools");
12
+ const project_1 = require("../native/project");
13
13
  async function assertSafeCleanTarget(projectRoot) {
14
14
  const root = await node_fs_1.default.promises.realpath(projectRoot);
15
- const plan = await (0, tools_1.resolveHarmonyBuildPlanIfPresentAsync)(root);
16
- const target = plan?.harmonyRoot ?? node_path_1.default.join(root, build_descriptor_1.HarmonyPlatformDirectory);
15
+ const plan = await (0, project_1.resolveHarmonyBuildPlanIfPresentAsync)(root);
16
+ const target = plan?.harmonyRoot ?? node_path_1.default.join(root, internal_1.HarmonyPlatformDirectory);
17
17
  let stat;
18
18
  try {
19
19
  stat = await node_fs_1.default.promises.lstat(target);
@@ -23,7 +23,7 @@ async function assertSafeCleanTarget(projectRoot) {
23
23
  return target;
24
24
  throw new errors_1.HarmonyCliError(error.code || 'ERR_HARMONY_CLEAN_TARGET', error.message || `Cannot inspect the Harmony clean target: ${target}. Please delete it manually.`, { cause: error, exitCode: error.exitCode, operation: error.operation });
25
25
  }
26
- if (!plan) {
26
+ if (!plan || plan.workflow !== 'cng') {
27
27
  throw new errors_1.HarmonyCliError('ERR_HARMONY_CLEAN_TARGET', `Refusing to clean ${target} because its CNG manifest is missing. Please delete it manually or run prebuild without --clean.`, { operation: 'clean' });
28
28
  }
29
29
  if (!stat.isDirectory() || stat.isSymbolicLink()) {
@@ -0,0 +1,2 @@
1
+ import { type Command } from '../command';
2
+ export declare const command: Command;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.command = void 0;
4
+ const command_1 = require("../command");
5
+ const errors_1 = require("../errors");
6
+ const projectLock_1 = require("../projectLock");
7
+ const options_1 = require("./options");
8
+ const command = async (argv, io) => {
9
+ const args = (0, command_1.parseCommandArgs)(args => (0, options_1.parsePrebuildArgs)(args, { allowProject: true }), argv, io);
10
+ if (!args)
11
+ return 0;
12
+ const { projectRoot: root, options } = args;
13
+ const { check, clean, passthrough } = options;
14
+ if (check && passthrough.length) {
15
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--check cannot be combined with mutating prebuild options.');
16
+ }
17
+ return (0, projectLock_1.withHarmonyProjectLockAsync)(root, check ? 'prebuild-check' : 'prebuild', async () => {
18
+ if (clean) {
19
+ const { assertSafeCleanTarget } = await import('./clean.js');
20
+ await assertSafeCleanTarget(root);
21
+ }
22
+ const { doctorAsync, formatDoctor } = await import('../doctor/doctor.js');
23
+ const doctor = await doctorAsync(root, {
24
+ requireBuildTools: false,
25
+ validateGeneratedProject: !clean,
26
+ validateModules: false,
27
+ });
28
+ if (!doctor.ok) {
29
+ io.error(formatDoctor(doctor));
30
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_DOCTOR_FAILED', 'Harmony doctor found blocking errors.', {
31
+ operation: 'doctor',
32
+ });
33
+ }
34
+ for (const item of doctor.checks.filter(item => item.status === 'warn'))
35
+ io.warn(`! ${item.message}`);
36
+ if (check) {
37
+ const { checkAsync } = await import('./check.js');
38
+ const result = await checkAsync(root);
39
+ if (result.clean)
40
+ io.log('Harmony CNG output is up to date.');
41
+ else
42
+ for (const change of result.changes)
43
+ io.log(`${change.type}: ${change.path}`);
44
+ return result.clean ? 0 : 2;
45
+ }
46
+ const { prebuildParsedAsync } = await import('./prebuild.js');
47
+ await prebuildParsedAsync(root, passthrough);
48
+ return 0;
49
+ });
50
+ };
51
+ exports.command = command;
@@ -6,13 +6,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.prebuildParsedAsync = prebuildParsedAsync;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const errors_1 = require("../errors");
9
+ const bare_1 = require("../native/bare");
9
10
  const process_1 = require("../process");
10
11
  const projectLock_1 = require("../projectLock");
11
12
  const expo_1 = require("../expo");
12
- const tools_1 = require("../tools");
13
+ const toolchain_1 = require("../native/toolchain");
14
+ const project_1 = require("../native/project");
13
15
  const clean_1 = require("./clean");
14
16
  const template_1 = require("./template");
15
17
  async function prebuildParsedUnlockedAsync(projectRoot, passthrough, options = {}) {
18
+ if ((0, bare_1.isBareHarmonyProject)(projectRoot)) {
19
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_BARE_PREBUILD', 'This Harmony project is manually maintained. Build it directly; prebuild cannot overwrite a bare project.', { operation: 'prebuild' });
20
+ }
16
21
  if (passthrough.includes('--clean'))
17
22
  await (0, clean_1.assertSafeCleanTarget)(projectRoot);
18
23
  const expo = (0, expo_1.resolveExpoCli)(projectRoot);
@@ -28,7 +33,7 @@ async function prebuildParsedUnlockedAsync(projectRoot, passthrough, options = {
28
33
  capture: Boolean(options.capture),
29
34
  cwd: projectRoot,
30
35
  env: {
31
- ...(0, tools_1.createHarmonyToolchainEnv)(),
36
+ ...(0, toolchain_1.createHarmonyToolchainEnv)(),
32
37
  ...packed.env,
33
38
  ...(options.buildType ? { EXPO_HARMONY_BUILD_TYPE: options.buildType } : {}),
34
39
  },
@@ -43,9 +48,12 @@ async function prebuildParsedUnlockedAsync(projectRoot, passthrough, options = {
43
48
  });
44
49
  }
45
50
  try {
46
- const plan = await (0, tools_1.resolveHarmonyBuildPlanAsync)(projectRoot, {
51
+ const plan = await (0, project_1.resolveHarmonyBuildPlanAsync)(projectRoot, {
47
52
  buildMode: options.buildType,
48
53
  });
54
+ if (plan.workflow !== 'cng') {
55
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_TEMPLATE_INVALID', 'Prebuild did not produce a CNG project.', { operation: 'prebuild' });
56
+ }
49
57
  await node_fs_1.default.promises.access(plan.projectFiles.templateMarker, node_fs_1.default.constants.R_OK);
50
58
  }
51
59
  catch (cause) {
@@ -14,10 +14,10 @@ function resolve(project) {
14
14
  const load = (0, node_module_1.createRequire)(node_path_1.default.join(project, 'package.json'));
15
15
  let api;
16
16
  try {
17
- api = load('@expo-harmony/prebuild-config/template');
17
+ api = load('@expo-harmony/prebuild-config/internal/template');
18
18
  }
19
19
  catch (cause) {
20
- throw new errors_1.HarmonyCliError('ERR_HARMONY_TEMPLATE_INVALID', 'Cannot load @expo-harmony/prebuild-config/template from the project. Update the project-local prebuild config to a compatible version.', { cause, operation: 'resolve-template' });
20
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_TEMPLATE_INVALID', 'Cannot load @expo-harmony/prebuild-config/internal/template from the project. Update the project-local prebuild config to a compatible version.', { cause, operation: 'resolve-template' });
21
21
  }
22
22
  if (typeof api?.ROOT_ENV !== 'string' || typeof api.resolveBundled !== 'function') {
23
23
  throw new errors_1.HarmonyCliError('ERR_HARMONY_TEMPLATE_INVALID', 'The project-local @expo-harmony/prebuild-config template API is invalid.', { operation: 'resolve-template' });
@@ -18,6 +18,15 @@ export interface ProcessResult {
18
18
  stdout: string;
19
19
  timedOut: boolean;
20
20
  }
21
+ interface CheckedProcessOptions {
22
+ code: string;
23
+ cwd: string;
24
+ env?: NodeJS.ProcessEnv;
25
+ message: string;
26
+ operation: string;
27
+ outputLimit?: number;
28
+ timeoutMs?: number;
29
+ }
21
30
  declare function formatDiagnostics(result: Pick<ProcessResult, 'stderr' | 'stdout'>, limit?: number): string;
22
31
  declare function spawnAsync(command: string, args: string[], options?: ProcessOptions): Promise<ProcessResult>;
23
32
  declare function startManagedProcess(command: string, args: string[], options?: ProcessOptions): {
@@ -28,4 +37,5 @@ declare function startManagedProcess(command: string, args: string[], options?:
28
37
  stop: (signal?: NodeJS.Signals, graceMs?: number) => Promise<ProcessResult>;
29
38
  wasStopped: () => boolean;
30
39
  };
31
- export { formatDiagnostics, spawnAsync, startManagedProcess };
40
+ declare function runCheckedAsync(command: string, args: string[], options: CheckedProcessOptions): Promise<ProcessResult>;
41
+ export { formatDiagnostics, runCheckedAsync, spawnAsync, startManagedProcess };
package/build/process.js CHANGED
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.formatDiagnostics = formatDiagnostics;
7
+ exports.runCheckedAsync = runCheckedAsync;
7
8
  exports.spawnAsync = spawnAsync;
8
9
  exports.startManagedProcess = startManagedProcess;
9
10
  const cross_spawn_1 = __importDefault(require("cross-spawn"));
@@ -306,3 +307,18 @@ function startManagedProcess(command, args, options = {}) {
306
307
  wasStopped: () => stopped,
307
308
  };
308
309
  }
310
+ async function runCheckedAsync(command, args, options) {
311
+ const result = await spawnAsync(command, args, {
312
+ capture: true,
313
+ cwd: options.cwd,
314
+ env: options.env,
315
+ operation: options.operation,
316
+ outputLimit: options.outputLimit || 2 * 1024 * 1024,
317
+ timeoutMs: options.timeoutMs,
318
+ });
319
+ if (result.code !== 0 || result.timedOut) {
320
+ const diagnostics = formatDiagnostics(result);
321
+ throw new errors_1.HarmonyCliError(options.code, `${options.message} exited with code ${result.code}${result.timedOut ? ' after timing out' : ''}.${diagnostics ? `\n${diagnostics}` : ''}`, { exitCode: result.code || 1, operation: options.operation });
322
+ }
323
+ return result;
324
+ }
@@ -0,0 +1 @@
1
+ export declare function timedAsync<T>(steps: Record<string, number>, name: string, operation: () => Promise<T> | T): Promise<T>;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.timedAsync = timedAsync;
4
+ async function timedAsync(steps, name, operation) {
5
+ const started = performance.now();
6
+ try {
7
+ return await operation();
8
+ }
9
+ finally {
10
+ steps[name] = Math.max(0, Math.round(performance.now() - started));
11
+ }
12
+ }
@@ -147,8 +147,13 @@ async function acquireHarmonyProjectLockAsync(projectRoot, operation) {
147
147
  };
148
148
  }
149
149
  catch (cause) {
150
- if (cause instanceof errors_1.HarmonyCliError)
151
- throw cause;
150
+ if (cause instanceof errors_1.HarmonyCliError) {
151
+ throw new errors_1.HarmonyCliError(cause.code, cause.message, {
152
+ cause,
153
+ exitCode: cause.exitCode,
154
+ operation: cause.operation,
155
+ });
156
+ }
152
157
  if (cause?.code !== 'EEXIST') {
153
158
  throw new errors_1.HarmonyCliError('ERR_HARMONY_PROJECT_LOCK', 'Cannot acquire the Harmony native operation lock.', { cause, operation: 'project-lock' });
154
159
  }
@@ -1,4 +1,4 @@
1
- import type { HarmonyTool } from '../tools';
1
+ import { type HarmonyTool } from '../native/toolchain';
2
2
  interface Device {
3
3
  aliases: string[];
4
4
  connectTool: string | null;
@@ -18,8 +18,6 @@ function parseHdcTargets(output) {
18
18
  throw new errors_1.HarmonyCliError('ERR_HARMONY_DEVICE_OUTPUT', 'HDC returned an unsupported target-list format.', { operation: 'list-devices' });
19
19
  }
20
20
  return {
21
- // Only the first HDC column is a selectable target name. The remaining
22
- // verbose columns describe transport, state, location and connect tool.
23
21
  aliases: [fields[0]],
24
22
  connectTool: fields[4] || null,
25
23
  id: fields[0],
@@ -32,10 +30,7 @@ function parseHdcTargets(output) {
32
30
  function hasCommandFailure(result) {
33
31
  const output = `${result.stdout || ''}\n${result.stderr || ''}`;
34
32
  return result.code !== 0 || result.timedOut
35
- // HDC occasionally reports transport and package-manager failures on
36
- // stdout while still returning exit code 0 (for example,
37
- // "Connect server failed"). Treat its documented failure vocabulary as
38
- // authoritative regardless of where it appears on the line.
33
+ // HDC can print failures to stdout while returning exit code 0.
39
34
  || /\[(?:Fail|Error)\]|\b(?:Failure|failed)\b|(?:失败|错误)/iu.test(output);
40
35
  }
41
36
  async function runHdcAsync(hdc, args, options = {}) {
@@ -93,7 +88,6 @@ async function selectDeviceAsync(hdc, requested, options = {}) {
93
88
  throw new errors_1.HarmonyCliError('ERR_HARMONY_DEVICE_NOT_FOUND', 'No connected Harmony device was reported by HDC.', { operation: 'select-device' });
94
89
  }
95
90
  const instances = await (0, emulators_1.listEmulatorsAsync)(options.emulator, { cwd: options.cwd });
96
- // Prefer an instance that is already booting when HDC is not connected yet.
97
91
  const running = instances.filter(instance => instance.running);
98
92
  const candidates = requested
99
93
  ? instances.filter(instance => instance.name === requested)
@@ -167,12 +161,23 @@ async function installHapAsync(hdc, device, hap, options = {}) {
167
161
  async function configureMetroPortAsync(hdc, device, port, options = {}) {
168
162
  const deviceEndpoint = `tcp:${options.devicePort || 8081}`;
169
163
  const hostEndpoint = `tcp:${port}`;
170
- await runHdcAsync(hdc, ['-t', device.id, 'fport', 'rm', deviceEndpoint, hostEndpoint], {
171
- allowFailure: true,
164
+ const forwards = await runHdcAsync(hdc, ['-t', device.id, 'fport', 'ls'], {
172
165
  cwd: options.cwd,
173
- operation: 'remove-metro-port',
166
+ operation: 'list-metro-ports',
174
167
  timeoutMs: 15_000,
175
168
  });
169
+ for (const line of forwards.stdout.split(/\r?\n/u)) {
170
+ const [target, remote, local, direction] = line.trim().split(/\s+/u);
171
+ if (target !== device.id || remote !== deviceEndpoint || direction !== '[Reverse]')
172
+ continue;
173
+ if (local === hostEndpoint)
174
+ return;
175
+ await runHdcAsync(hdc, ['-t', device.id, 'fport', 'rm', remote, local], {
176
+ cwd: options.cwd,
177
+ operation: 'remove-metro-port',
178
+ timeoutMs: 15_000,
179
+ });
180
+ }
176
181
  await runHdcAsync(hdc, ['-t', device.id, 'rport', deviceEndpoint, hostEndpoint], {
177
182
  code: 'ERR_HARMONY_METRO_FORWARD',
178
183
  cwd: options.cwd,
@@ -1,4 +1,4 @@
1
- import type { HarmonyTool } from '../tools';
1
+ import { type HarmonyTool } from '../native/toolchain';
2
2
  interface HarmonyEmulator {
3
3
  name: string;
4
4
  running: boolean;
@@ -51,9 +51,7 @@ function startEmulator(tool, name, logFile, cwd) {
51
51
  const log = node_fs_1.default.openSync(logFile, 'w', 0o600);
52
52
  let failure = null;
53
53
  try {
54
- // Emulator may remain alive for the whole GUI session. Give it its own
55
- // process group and file-backed output so it survives the CLI/Metro exit.
56
- // Default instance/image paths match the ones used by -list -details.
54
+ // A detached process with file output lets the emulator survive CLI exit.
57
55
  const child = (0, cross_spawn_1.default)(tool.command, [...tool.args, '-start', name], {
58
56
  cwd,
59
57
  detached: true,
@@ -65,8 +63,7 @@ function startEmulator(tool, name, logFile, cwd) {
65
63
  failure = cause.message;
66
64
  });
67
65
  child.once('exit', (code, signal) => {
68
- // Some versions use a short-lived launcher. A zero exit alone does not
69
- // prove readiness; the caller still checks the guest's name and boot state.
66
+ // Some launchers exit before the guest boots; HDC still has to confirm readiness.
70
67
  if (code !== 0)
71
68
  failure = signal ? `terminated by ${signal}` : `exited with code ${code}`;
72
69
  });
@@ -0,0 +1,2 @@
1
+ import { type Command } from '../command';
2
+ export declare const command: Command;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.command = void 0;
4
+ const command_1 = require("../command");
5
+ const options_1 = require("./options");
6
+ const command = async (argv, io) => {
7
+ const args = (0, command_1.parseCommandArgs)(options_1.parseRunArgs, argv, io);
8
+ if (!args)
9
+ return 0;
10
+ const { projectRoot: root, options } = args;
11
+ const { runHarmonySessionAsync } = await import('./run.js');
12
+ const session = await runHarmonySessionAsync(root, {
13
+ ...options,
14
+ interactiveBundler: true,
15
+ io,
16
+ });
17
+ const { result } = session;
18
+ io.log(`Launched ${result.bundleName} on ${result.device.id} (${result.variant}).`);
19
+ if (session.metro.owner === 'started') {
20
+ io.log('\n› Logs for your project will appear below. Press Ctrl+C to exit.');
21
+ await session.metro.waitAsync();
22
+ }
23
+ return 0;
24
+ };
25
+ exports.command = command;
@@ -102,7 +102,7 @@ async function startExpoMetroAsync(projectRoot, options) {
102
102
  exitError = error;
103
103
  });
104
104
  const startedAt = Date.now();
105
- const timeoutMs = options.readyTimeoutMs || 60_000;
105
+ const timeoutMs = options.readyTimeoutMs || 1800000;
106
106
  try {
107
107
  while (Date.now() - startedAt < timeoutMs) {
108
108
  if (exitError instanceof errors_1.HarmonyCliError) {