@expo-harmony/expo-modules-autolinking 55.0.25-harmony.2 → 55.0.25-harmony.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,12 @@
4
4
 
5
5
  为 HarmonyOS 上的 React Native 应用提供 Expo Modules 和 RNOH 原生模块自动链接能力,与官方同版本的 expo-modules-autolinking 配套使用。
6
6
 
7
+ ## 安装
8
+
9
+ ```bash
10
+ npm install @expo-harmony/expo-modules-autolinking @react-native-oh/react-native-harmony-cli@^0.84.1
11
+ ```
12
+
7
13
  ## Author
8
14
 
9
15
  **expo-harmony** © [Baoshuo](https://github.com/renbaoshuo), Released under the MIT License.<br>
@@ -32,12 +32,17 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
36
39
  exports.materializeLocalSourcesAsync = materializeLocalSourcesAsync;
37
40
  exports.materializeModuleArtifactAsync = materializeModuleArtifactAsync;
38
41
  const fs = __importStar(require("node:fs"));
39
42
  const path = __importStar(require("node:path"));
40
- const node_child_process_1 = require("node:child_process");
43
+ const cross_spawn_1 = __importDefault(require("cross-spawn"));
44
+ const toolCommand_1 = require("../utilities/toolCommand");
45
+ const terminateProcess_1 = require("../utilities/terminateProcess");
41
46
  const errors_1 = require("../errors");
42
47
  const values_1 = require("../utilities/values");
43
48
  const publish_1 = require("./transaction/publish");
@@ -45,13 +50,6 @@ const DefaultBuildTimeoutMs = 10 * 60_000;
45
50
  const DefaultOutputLimit = 64 * 1024;
46
51
  const HarmonyProjectPath = 'harmony';
47
52
  const HarmonyBuildOutputPath = 'library/build/default/outputs/default/library.har';
48
- function materializationError(code, message, options, details) {
49
- throw new errors_1.HarmonyAutolinkingError(code, message, {
50
- packageName: options.packageName,
51
- stage: 'artifact-materialize',
52
- details,
53
- });
54
- }
55
53
  function createFixedHvigorBuildDescriptor(projectPath, buildType) {
56
54
  return {
57
55
  executable: 'hvigorw',
@@ -101,7 +99,11 @@ async function validateBundledArtifactsAsync(options) {
101
99
  for (const relative of harPaths) {
102
100
  const harPath = path.resolve(options.packageRoot, relative);
103
101
  if (!await isNonEmptyRegularHarAsync(harPath, options.packageRoot, options.packageName)) {
104
- materializationError('SOURCE_ARTIFACT_NOT_PUBLISHED', `${options.packageName} has a missing or empty Harmony HAR.`, options, { harPath: relative });
102
+ throw new errors_1.HarmonyAutolinkingError('SOURCE_ARTIFACT_NOT_PUBLISHED', `${options.packageName} has a missing or empty Harmony HAR.`, {
103
+ packageName: options.packageName,
104
+ stage: 'artifact-materialize',
105
+ details: { harPath: relative },
106
+ });
105
107
  }
106
108
  }
107
109
  return { kind: 'bundled', harPaths };
@@ -153,35 +155,28 @@ function fixedLocalSourceBuildSteps(build) {
153
155
  ];
154
156
  }
155
157
  function resolveFixedBuildStep(step, env) {
156
- if (step.id === 'ohpm-install') {
157
- return env.HARMONY_OHPM ? { ...step, executable: env.HARMONY_OHPM } : step;
158
- }
159
- const configured = env.HARMONY_HVIGORW;
160
- if (configured) {
161
- return /\.(?:c|m)?js$/iu.test(configured)
162
- ? { ...step, executable: env.HARMONY_NODE || process.execPath, args: [configured, ...step.args] }
163
- : { ...step, executable: configured };
164
- }
165
- return step;
158
+ const invocation = (0, toolCommand_1.resolveHarmonyCommand)(step.executable, step.args, env);
159
+ return { ...step, executable: invocation.command, args: invocation.args };
166
160
  }
167
161
  async function runFixedBuildStepAsync(step, packageName, options) {
168
162
  const timeoutMs = options.timeoutMs ?? DefaultBuildTimeoutMs;
169
163
  const outputLimit = options.outputLimit ?? DefaultOutputLimit;
170
164
  const stage = `artifact-materialize:${step.id}`;
171
- const env = { ...process.env, ...options.env };
172
- const resolvedStep = resolveFixedBuildStep(step, env);
173
165
  if (!Number.isInteger(timeoutMs) || timeoutMs <= 0
174
166
  || !Number.isInteger(outputLimit) || outputLimit <= 0) {
175
167
  throw new errors_1.HarmonyAutolinkingError('INVALID_OPTIONS', 'timeoutMs and outputLimit must be positive integers.', { packageName, stage });
176
168
  }
169
+ const env = { ...process.env, ...options.env };
170
+ const command = resolveFixedBuildStep(step, env);
177
171
  const result = await new Promise((resolve, reject) => {
178
172
  let output = '';
179
173
  let timedOut = false;
180
174
  let settled = false;
181
- let killTimer;
175
+ let stopping;
176
+ let escalation;
182
177
  let child;
183
178
  try {
184
- child = (0, node_child_process_1.spawn)(resolvedStep.executable, [...resolvedStep.args], {
179
+ child = (0, cross_spawn_1.default)(command.executable, [...command.args], {
185
180
  cwd: step.cwd,
186
181
  env,
187
182
  shell: false,
@@ -198,30 +193,49 @@ async function runFixedBuildStepAsync(step, packageName, options) {
198
193
  child.stderr.on('data', (chunk) => {
199
194
  output = appendBounded(output, chunk, outputLimit);
200
195
  });
196
+ const finish = async (code, signal, error) => {
197
+ try {
198
+ await stopping;
199
+ }
200
+ catch (cause) {
201
+ error = cause;
202
+ }
203
+ if (settled)
204
+ return;
205
+ settled = true;
206
+ clearTimeout(timeout);
207
+ if (escalation)
208
+ clearTimeout(escalation);
209
+ if (error)
210
+ reject(error);
211
+ else
212
+ resolve({ code, signal, output, timedOut });
213
+ };
201
214
  const timeout = setTimeout(() => {
202
215
  timedOut = true;
203
- child.kill('SIGTERM');
204
- killTimer = setTimeout(() => child.kill('SIGKILL'), 1_000);
205
- killTimer.unref?.();
216
+ stopping = Promise.resolve().then(() => (0, terminateProcess_1.terminateProcess)(child, 'SIGTERM'));
217
+ if (process.platform === 'win32') {
218
+ void finish(null, 'SIGTERM');
219
+ }
220
+ else {
221
+ stopping.catch((error) => {
222
+ void finish(null, null, error);
223
+ });
224
+ escalation = setTimeout(() => {
225
+ stopping = (0, terminateProcess_1.terminateProcess)(child, 'SIGKILL');
226
+ stopping.catch((error) => {
227
+ void finish(null, null, error);
228
+ });
229
+ }, 1_000);
230
+ escalation.unref?.();
231
+ }
206
232
  }, timeoutMs);
207
233
  timeout.unref?.();
208
234
  child.on('error', (cause) => {
209
- if (settled)
210
- return;
211
- settled = true;
212
- clearTimeout(timeout);
213
- if (killTimer)
214
- clearTimeout(killTimer);
215
- reject(cause);
235
+ void finish(null, null, cause);
216
236
  });
217
237
  child.on('close', (code, signal) => {
218
- if (settled)
219
- return;
220
- settled = true;
221
- clearTimeout(timeout);
222
- if (killTimer)
223
- clearTimeout(killTimer);
224
- resolve({ code, signal, output, timedOut });
238
+ void finish(code, signal);
225
239
  });
226
240
  }).catch((cause) => {
227
241
  throw new errors_1.HarmonyAutolinkingError('SOURCE_BUILD_FAILED', `Unable to execute the fixed ${step.label} for local Harmony source at ${step.cwd}.`, { cause, packageName, stage });
@@ -252,8 +266,8 @@ async function materializeLocalSourceAsync(options) {
252
266
  for (const step of fixedLocalSourceBuildSteps(artifact.build)) {
253
267
  await runFixedBuildStepAsync(step, module.packageName, options);
254
268
  }
255
- const outputReady = await isNonEmptyRegularHarAsync(artifact.outputPath, artifact.build.cwd, module.packageName);
256
- if (!outputReady) {
269
+ const ready = await isNonEmptyRegularHarAsync(artifact.outputPath, artifact.build.cwd, module.packageName);
270
+ if (!ready) {
257
271
  throw new errors_1.HarmonyAutolinkingError('SOURCE_BUILD_FAILED', `Hvigor did not produce a non-empty HAR at ${(0, values_1.normalizeSlashes)(path.relative(packageRoot, artifact.outputPath))}.`, { packageName: module.packageName, stage: 'artifact-materialize' });
258
272
  }
259
273
  await (0, publish_1.publishArtifactsAsync)({
@@ -1,2 +1,5 @@
1
- declare function resolveCliAsync(cliOptions: any): Promise<string>;
1
+ declare function resolveCliAsync(options: any): Promise<{
2
+ executable: string;
3
+ args: string[];
4
+ }>;
2
5
  export { resolveCliAsync, };
@@ -9,47 +9,70 @@ const node_path_1 = __importDefault(require("node:path"));
9
9
  const constants_1 = require("../../config/constants");
10
10
  const errors_1 = require("../../errors");
11
11
  const values_1 = require("../../utilities/values");
12
- function findPackageJson(projectRoot, packageName) {
12
+ function findPackageJson(root, packageName) {
13
13
  try {
14
- return (0, values_1.resolvePackageFromProject)(projectRoot, `${packageName}/package.json`);
14
+ return (0, values_1.resolvePackageFromProject)(root, `${packageName}/package.json`);
15
15
  }
16
16
  catch (cause) {
17
- throw new errors_1.HarmonyAutolinkingError('RNOH_CLI_NOT_FOUND', `${constants_1.RnohCliPackage} is not installed.`, {
17
+ throw new errors_1.HarmonyAutolinkingError('RNOH_CLI_NOT_FOUND', `${packageName} is not installed.`, {
18
18
  cause,
19
19
  stage: 'rnoh-preflight',
20
20
  });
21
21
  }
22
22
  }
23
- async function resolveCliAsync(cliOptions) {
24
- const projectRoot = await (0, values_1.realpathExistingAsync)(cliOptions.projectRoot, {
23
+ async function resolveCliAsync(options) {
24
+ const root = await (0, values_1.realpathExistingAsync)(options.projectRoot, {
25
25
  type: 'directory',
26
26
  field: 'projectRoot',
27
27
  stage: 'rnoh-preflight',
28
28
  });
29
- const raw = cliOptions.rnohCliPackageJsonPath
30
- || findPackageJson(projectRoot, constants_1.RnohCliPackage);
31
- const pkg = await (0, values_1.realpathExistingAsync)(raw, {
29
+ const raw = options.rnohCliPackageJsonPath
30
+ || findPackageJson(root, constants_1.RnohCliPackage);
31
+ const manifest = await (0, values_1.realpathExistingAsync)(raw, {
32
32
  type: 'file',
33
33
  code: 'RNOH_CLI_NOT_FOUND',
34
34
  field: 'RNOH CLI package.json',
35
35
  stage: 'rnoh-preflight',
36
36
  });
37
- const packageJson = await (0, values_1.readJsonAsync)(pkg, 'RNOH_CLI_NOT_FOUND', 'rnoh-preflight');
38
- if (packageJson.name !== constants_1.RnohCliPackage) {
39
- throw new errors_1.HarmonyAutolinkingError('RNOH_CLI_NOT_FOUND', `Expected ${constants_1.RnohCliPackage}, found ${String(packageJson.name)}.`, { stage: 'rnoh-preflight' });
37
+ const plugin = await (0, values_1.readJsonAsync)(manifest, 'RNOH_CLI_NOT_FOUND', 'rnoh-preflight');
38
+ if (plugin.name !== constants_1.RnohCliPackage) {
39
+ throw new errors_1.HarmonyAutolinkingError('RNOH_CLI_NOT_FOUND', `Expected ${constants_1.RnohCliPackage}, found ${String(plugin.name)}.`, { stage: 'rnoh-preflight' });
40
40
  }
41
- const pluginRoot = node_path_1.default.dirname(pkg);
42
- const pluginConfig = node_path_1.default.join(pluginRoot, 'react-native.config.js');
43
- if (!node_fs_1.default.existsSync(pluginConfig)) {
41
+ const config = node_path_1.default.join(node_path_1.default.dirname(manifest), 'react-native.config.js');
42
+ if (!node_fs_1.default.existsSync(config)) {
44
43
  throw new errors_1.HarmonyAutolinkingError('RNOH_CLI_NOT_FOUND', `${constants_1.RnohCliPackage} does not expose its public React Native CLI plugin config.`, { stage: 'rnoh-preflight' });
45
44
  }
46
- const exec = cliOptions.reactNativeExecutable
47
- || node_path_1.default.join(cliOptions.nodeModulesPath || node_path_1.default.join(projectRoot, 'node_modules'), '.bin', 'react-native');
48
- const executable = await (0, values_1.realpathExistingAsync)(exec, {
45
+ if (options.reactNativeExecutable) {
46
+ const executable = await (0, values_1.realpathExistingAsync)(options.reactNativeExecutable, {
47
+ type: 'file',
48
+ code: 'RNOH_CLI_NOT_FOUND',
49
+ field: 'project-local react-native executable',
50
+ stage: 'rnoh-preflight',
51
+ });
52
+ return { executable, args: [] };
53
+ }
54
+ // Resolve the package itself, not a package-manager-generated .bin shell shim.
55
+ // The real manifest path also handles symlinked packages (for example pnpm).
56
+ const file = await (0, values_1.realpathExistingAsync)(options.nodeModulesPath
57
+ ? node_path_1.default.join(options.nodeModulesPath, 'react-native', 'package.json')
58
+ : findPackageJson(root, 'react-native'), {
59
+ type: 'file',
60
+ code: 'RNOH_CLI_NOT_FOUND',
61
+ field: 'project-local react-native package.json',
62
+ stage: 'rnoh-preflight',
63
+ });
64
+ const pkg = await (0, values_1.readJsonAsync)(file, 'RNOH_CLI_NOT_FOUND', 'rnoh-preflight');
65
+ const bin = typeof pkg.bin === 'string'
66
+ ? pkg.bin
67
+ : pkg.bin?.['react-native'];
68
+ if (typeof bin !== 'string' || !bin.trim()) {
69
+ throw new errors_1.HarmonyAutolinkingError('RNOH_CLI_NOT_FOUND', 'The project-local react-native package does not declare a react-native bin entry.', { stage: 'rnoh-preflight' });
70
+ }
71
+ const entry = await (0, values_1.realpathExistingAsync)(node_path_1.default.resolve(node_path_1.default.dirname(file), bin), {
49
72
  type: 'file',
50
73
  code: 'RNOH_CLI_NOT_FOUND',
51
74
  field: 'project-local react-native executable',
52
75
  stage: 'rnoh-preflight',
53
76
  });
54
- return executable;
77
+ return { executable: process.execPath, args: [entry] };
55
78
  }
@@ -4,13 +4,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.linkRnohAsync = linkRnohAsync;
7
- const node_child_process_1 = __importDefault(require("node:child_process"));
7
+ const cross_spawn_1 = __importDefault(require("cross-spawn"));
8
8
  const node_fs_1 = __importDefault(require("node:fs"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
10
  const constants_1 = require("../../config/constants");
11
11
  const errors_1 = require("../../errors");
12
12
  const options_1 = require("../../config/options");
13
13
  const values_1 = require("../../utilities/values");
14
+ const terminateProcess_1 = require("../../utilities/terminateProcess");
14
15
  const generate_1 = require("../manifest/generate");
15
16
  const canonicalize_1 = require("../persistence/canonicalize");
16
17
  const packageMetadata_1 = require("./packageMetadata");
@@ -37,13 +38,14 @@ function spawnBoundedAsync(executable, argv, options = {}) {
37
38
  let stdoutTruncated = false;
38
39
  let stderrTruncated = false;
39
40
  let timedOut = false;
40
- let killTimer;
41
+ let escalation;
41
42
  // eslint-disable-next-line prefer-const
42
43
  let timeout;
43
44
  let settled = false;
45
+ let stopping;
44
46
  let child;
45
47
  try {
46
- child = node_child_process_1.default.spawn(executable, argv, {
48
+ child = (0, cross_spawn_1.default)(executable, argv, {
47
49
  cwd: options.cwd,
48
50
  env: options.env || process.env,
49
51
  shell: false,
@@ -64,36 +66,56 @@ function spawnBoundedAsync(executable, argv, options = {}) {
64
66
  stderrTruncated = true;
65
67
  stderr = appendBounded(stderr, chunk, outputLimit);
66
68
  });
67
- child.on('error', (error) => {
69
+ const finish = async (code, signal, error) => {
70
+ try {
71
+ await stopping;
72
+ }
73
+ catch (cause) {
74
+ error = cause;
75
+ }
68
76
  if (settled)
69
77
  return;
70
78
  settled = true;
71
79
  clearTimeout(timeout);
72
- clearTimeout(killTimer);
73
- reject(error);
80
+ clearTimeout(escalation);
81
+ if (error)
82
+ reject(error);
83
+ else
84
+ resolve({
85
+ code,
86
+ signal,
87
+ timedOut,
88
+ stdout: stdout.toString('utf8'),
89
+ stderr: stderr.toString('utf8'),
90
+ stdoutTruncated,
91
+ stderrTruncated,
92
+ });
93
+ };
94
+ child.on('error', (error) => {
95
+ void finish(null, null, error);
74
96
  });
75
97
  timeout = setTimeout(() => {
76
98
  timedOut = true;
77
- child.kill('SIGTERM');
78
- killTimer = setTimeout(() => child.kill('SIGKILL'), 1_000);
79
- killTimer.unref?.();
99
+ stopping = Promise.resolve().then(() => (0, terminateProcess_1.terminateProcess)(child, 'SIGTERM'));
100
+ if (process.platform === 'win32') {
101
+ void finish(null, 'SIGTERM');
102
+ }
103
+ else {
104
+ stopping.catch((error) => {
105
+ void finish(null, null, error);
106
+ });
107
+ escalation = setTimeout(() => {
108
+ stopping = (0, terminateProcess_1.terminateProcess)(child, 'SIGKILL');
109
+ stopping.catch((error) => {
110
+ void finish(null, null, error);
111
+ });
112
+ }, 1_000);
113
+ escalation.unref?.();
114
+ }
80
115
  }, timeoutMs);
81
116
  timeout.unref?.();
82
117
  child.on('close', (code, signal) => {
83
- if (settled)
84
- return;
85
- settled = true;
86
- clearTimeout(timeout);
87
- clearTimeout(killTimer);
88
- resolve({
89
- code,
90
- signal,
91
- timedOut,
92
- stdout: stdout.toString('utf8'),
93
- stderr: stderr.toString('utf8'),
94
- stdoutTruncated,
95
- stderrTruncated,
96
- });
118
+ void finish(code, signal);
97
119
  });
98
120
  });
99
121
  }
@@ -335,7 +357,7 @@ function patchOhpmManifest(artifact, descriptors, buildType, harmonyProjectPath)
335
357
  }).source;
336
358
  }
337
359
  async function linkRnohAsync(linkOptions) {
338
- const executable = await (0, cli_1.resolveCliAsync)({
360
+ const command = await (0, cli_1.resolveCliAsync)({
339
361
  ...linkOptions,
340
362
  nodeModulesPath: linkOptions.commandNodeModulesPath || linkOptions.nodeModulesPath,
341
363
  });
@@ -347,12 +369,12 @@ async function linkRnohAsync(linkOptions) {
347
369
  };
348
370
  let result;
349
371
  try {
350
- result = await spawnBoundedAsync(executable, buildLinkCommandArgs({
351
- harmonyProjectPath: linkOptions.stageHarmonyProjectPath,
352
- nodeModulesPath: linkOptions.nodeModulesPath,
353
- include: linkOptions.include,
354
- exclude: linkOptions.exclude,
355
- }), spawn);
372
+ result = await spawnBoundedAsync(command.executable, [...command.args, ...buildLinkCommandArgs({
373
+ harmonyProjectPath: linkOptions.stageHarmonyProjectPath,
374
+ nodeModulesPath: linkOptions.nodeModulesPath,
375
+ include: linkOptions.include,
376
+ exclude: linkOptions.exclude,
377
+ })], spawn);
356
378
  }
357
379
  catch (cause) {
358
380
  throw new errors_1.HarmonyAutolinkingError('RNOH_LINK_FAILED', 'Unable to execute React Native link-harmony.', { cause, stage: 'rnoh-link' });
@@ -0,0 +1,3 @@
1
+ import { type ChildProcess } from 'node:child_process';
2
+ /** Stop a Windows command and its descendants before callers restore build inputs. */
3
+ export declare function terminateProcess(child: ChildProcess, signal: NodeJS.Signals): Promise<void>;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.terminateProcess = terminateProcess;
7
+ const node_child_process_1 = require("node:child_process");
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ /** Stop a Windows command and its descendants before callers restore build inputs. */
10
+ async function terminateProcess(child, signal) {
11
+ if (!child.pid)
12
+ return;
13
+ if (process.platform !== 'win32') {
14
+ child.kill(signal);
15
+ return;
16
+ }
17
+ try {
18
+ await new Promise((resolve, reject) => {
19
+ const command = node_path_1.default.win32.join(process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows', 'System32', 'taskkill.exe');
20
+ const killer = (0, node_child_process_1.spawn)(command, ['/pid', String(child.pid), '/T', '/F'], {
21
+ shell: false,
22
+ windowsHide: true,
23
+ stdio: 'ignore',
24
+ });
25
+ const timer = setTimeout(() => {
26
+ try {
27
+ killer.kill('SIGKILL');
28
+ }
29
+ catch { /* Still report the bounded termination failure. */ }
30
+ killer.unref();
31
+ reject(new Error(`Timed out terminating process tree ${child.pid}.`));
32
+ }, 5_000);
33
+ killer.once('error', (error) => {
34
+ clearTimeout(timer);
35
+ reject(error);
36
+ });
37
+ killer.once('exit', (code) => {
38
+ clearTimeout(timer);
39
+ if (code === 0)
40
+ resolve();
41
+ else
42
+ reject(new Error(`taskkill failed for process tree ${child.pid} (exit ${code}).`));
43
+ });
44
+ });
45
+ }
46
+ catch (cause) {
47
+ // A failed taskkill must be reported, but must not keep the caller hung forever.
48
+ child.unref();
49
+ throw new Error(`Unable to terminate process tree ${child.pid}.`, { cause });
50
+ }
51
+ finally {
52
+ // Descendants can retain pipe handles after the immediate child has exited.
53
+ // taskkill has finished (or failed explicitly); do not wait forever for EOF.
54
+ child.stdin?.destroy();
55
+ child.stdout?.destroy();
56
+ child.stderr?.destroy();
57
+ }
58
+ }
@@ -0,0 +1,6 @@
1
+ /** Keep tool overrides consistent across CLI, workspace and local HAR builds. */
2
+ export declare function resolveHarmonyCommand(command: string, args: readonly string[], env?: NodeJS.ProcessEnv): {
3
+ command: string;
4
+ args: string[];
5
+ };
6
+ export { terminateProcess } from './terminateProcess';
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.terminateProcess = void 0;
4
+ exports.resolveHarmonyCommand = resolveHarmonyCommand;
5
+ /** Keep tool overrides consistent across CLI, workspace and local HAR builds. */
6
+ function resolveHarmonyCommand(command, args, env = process.env) {
7
+ const configured = command === 'ohpm'
8
+ ? env.HARMONY_OHPM
9
+ : command === 'hvigorw' ? env.HARMONY_HVIGORW : undefined;
10
+ if (!configured)
11
+ return { command, args: [...args] };
12
+ const node = command === 'ohpm' ? env.HARMONY_OHPM_NODE : env.HARMONY_HVIGOR_NODE;
13
+ return /\.(?:c|m)?js$/iu.test(configured)
14
+ ? { command: node || env.HARMONY_NODE || process.execPath, args: [configured, ...args] }
15
+ : { command: configured, args: [...args] };
16
+ }
17
+ var terminateProcess_1 = require("./terminateProcess");
18
+ Object.defineProperty(exports, "terminateProcess", { enumerable: true, get: function () { return terminateProcess_1.terminateProcess; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo-harmony/expo-modules-autolinking",
3
- "version": "55.0.25-harmony.2",
3
+ "version": "55.0.25-harmony.3",
4
4
  "description": "HarmonyOS port of expo-modules-autolinking",
5
5
  "keywords": [
6
6
  "react-native",
@@ -20,7 +20,7 @@
20
20
  "main": "build/index.js",
21
21
  "types": "build/index.d.ts",
22
22
  "scripts": {
23
- "build": "rm -rf build && tsc -p tsconfig.json && chmod +x build/bin/expo-harmony-autolinking.js",
23
+ "build": "node ./scripts/build.mjs",
24
24
  "prepare": "yarn build",
25
25
  "typecheck": "tsc --noEmit -p tsconfig.json"
26
26
  },
@@ -30,6 +30,10 @@
30
30
  "require": "./build/index.js",
31
31
  "default": "./build/index.js"
32
32
  },
33
+ "./tool-command": {
34
+ "types": "./build/src/utilities/toolCommand.d.ts",
35
+ "default": "./build/src/utilities/toolCommand.js"
36
+ },
33
37
  "./harmony-module.schema.json": {
34
38
  "default": "./harmony-module.schema.json"
35
39
  }
@@ -40,6 +44,7 @@
40
44
  "dependencies": {
41
45
  "case": "1.6.3",
42
46
  "commander": "7.2.0",
47
+ "cross-spawn": "^7.0.6",
43
48
  "expo-modules-autolinking": "55.0.25",
44
49
  "json5": "2.2.3"
45
50
  },
@@ -52,11 +57,6 @@
52
57
  "peerDependencies": {
53
58
  "@react-native-oh/react-native-harmony-cli": "^0.84.1"
54
59
  },
55
- "peerDependenciesMeta": {
56
- "@react-native-oh/react-native-harmony-cli": {
57
- "optional": true
58
- }
59
- },
60
60
  "engines": {
61
61
  "node": ">=20"
62
62
  },
@@ -0,0 +1,27 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { chmodSync, rmSync, statSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const require = createRequire(import.meta.url);
8
+ const root = fileURLToPath(new URL('../', import.meta.url));
9
+
10
+ rmSync(path.join(root, 'build'), { recursive: true, force: true });
11
+
12
+ const result = spawnSync(process.execPath, [require.resolve('typescript/bin/tsc'), '-p', 'tsconfig.json'], {
13
+ cwd: root,
14
+ stdio: 'inherit',
15
+ shell: false,
16
+ });
17
+
18
+ if (result.error) {
19
+ throw new Error('Unable to start the TypeScript compiler.', { cause: result.error });
20
+ }
21
+
22
+ if (result.status !== 0) {
23
+ process.exitCode = result.status ?? 1;
24
+ } else if (process.platform !== 'win32') {
25
+ const file = path.join(root, 'build/bin/expo-harmony-autolinking.js');
26
+ chmodSync(file, statSync(file).mode | 0o111);
27
+ }
@@ -1,6 +1,8 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
- import { spawn } from 'node:child_process';
3
+ import spawn from 'cross-spawn';
4
+ import { resolveHarmonyCommand } from '../utilities/toolCommand';
5
+ import { terminateProcess } from '../utilities/terminateProcess';
4
6
 
5
7
  import { HarmonyAutolinkingError } from '../errors';
6
8
  import type {
@@ -38,19 +40,6 @@ interface MaterializeModuleArtifactOptions {
38
40
  readonly rnoh: RnohMetadata;
39
41
  }
40
42
 
41
- function materializationError(
42
- code: string,
43
- message: string,
44
- options: MaterializeModuleArtifactOptions,
45
- details?: unknown
46
- ): never {
47
- throw new HarmonyAutolinkingError(code, message, {
48
- packageName: options.packageName,
49
- stage: 'artifact-materialize',
50
- details,
51
- });
52
- }
53
-
54
43
  function createFixedHvigorBuildDescriptor(
55
44
  projectPath: string,
56
45
  buildType: BuildType
@@ -114,6 +103,7 @@ async function isNonEmptyRegularHarAsync(
114
103
  { packageName, stage: 'artifact-materialize' }
115
104
  );
116
105
  }
106
+
117
107
  return stat.size > 0;
118
108
  }
119
109
 
@@ -124,17 +114,22 @@ async function validateBundledArtifactsAsync(
124
114
  ...options.rnoh.harPaths,
125
115
  ...(options.arkTs ? [options.arkTs.harPath] : []),
126
116
  ])].sort(compareText);
117
+
127
118
  for (const relative of harPaths) {
128
119
  const harPath = path.resolve(options.packageRoot, relative);
129
120
  if (!await isNonEmptyRegularHarAsync(harPath, options.packageRoot, options.packageName)) {
130
- materializationError(
121
+ throw new HarmonyAutolinkingError(
131
122
  'SOURCE_ARTIFACT_NOT_PUBLISHED',
132
123
  `${options.packageName} has a missing or empty Harmony HAR.`,
133
- options,
134
- { harPath: relative }
124
+ {
125
+ packageName: options.packageName,
126
+ stage: 'artifact-materialize',
127
+ details: { harPath: relative },
128
+ }
135
129
  );
136
130
  }
137
131
  }
132
+
138
133
  return { kind: 'bundled', harPaths };
139
134
  }
140
135
 
@@ -162,6 +157,7 @@ async function materializeModuleArtifactAsync(
162
157
  packageName: options.packageName,
163
158
  mustExist: false,
164
159
  });
160
+
165
161
  return {
166
162
  kind: 'local-source',
167
163
  outputPath,
@@ -193,18 +189,8 @@ function fixedLocalSourceBuildSteps(build: FixedHvigorBuildDescriptor): Readonly
193
189
  }
194
190
 
195
191
  function resolveFixedBuildStep(step: FixedBuildStep, env: NodeJS.ProcessEnv): FixedBuildStep {
196
- if (step.id === 'ohpm-install') {
197
- return env.HARMONY_OHPM ? { ...step, executable: env.HARMONY_OHPM } : step;
198
- }
199
-
200
- const configured = env.HARMONY_HVIGORW;
201
- if (configured) {
202
- return /\.(?:c|m)?js$/iu.test(configured)
203
- ? { ...step, executable: env.HARMONY_NODE || process.execPath, args: [configured, ...step.args] }
204
- : { ...step, executable: configured };
205
- }
206
-
207
- return step;
192
+ const invocation = resolveHarmonyCommand(step.executable, step.args, env);
193
+ return { ...step, executable: invocation.command, args: invocation.args };
208
194
  }
209
195
 
210
196
  async function runFixedBuildStepAsync(
@@ -215,8 +201,6 @@ async function runFixedBuildStepAsync(
215
201
  const timeoutMs = options.timeoutMs ?? DefaultBuildTimeoutMs;
216
202
  const outputLimit = options.outputLimit ?? DefaultOutputLimit;
217
203
  const stage = `artifact-materialize:${step.id}`;
218
- const env = { ...process.env, ...options.env };
219
- const resolvedStep = resolveFixedBuildStep(step, env);
220
204
  if (!Number.isInteger(timeoutMs) || timeoutMs <= 0
221
205
  || !Number.isInteger(outputLimit) || outputLimit <= 0) {
222
206
  throw new HarmonyAutolinkingError(
@@ -226,6 +210,9 @@ async function runFixedBuildStepAsync(
226
210
  );
227
211
  }
228
212
 
213
+ const env = { ...process.env, ...options.env };
214
+ const command = resolveFixedBuildStep(step, env);
215
+
229
216
  const result = await new Promise<{
230
217
  code: number | null;
231
218
  signal: NodeJS.Signals | null;
@@ -235,10 +222,12 @@ async function runFixedBuildStepAsync(
235
222
  let output = '';
236
223
  let timedOut = false;
237
224
  let settled = false;
238
- let killTimer: NodeJS.Timeout | undefined;
225
+ let stopping: Promise<void> | undefined;
226
+ let escalation: NodeJS.Timeout | undefined;
239
227
  let child;
228
+
240
229
  try {
241
- child = spawn(resolvedStep.executable, [...resolvedStep.args], {
230
+ child = spawn(command.executable, [...command.args], {
242
231
  cwd: step.cwd,
243
232
  env,
244
233
  shell: false,
@@ -248,32 +237,55 @@ async function runFixedBuildStepAsync(
248
237
  reject(cause);
249
238
  return;
250
239
  }
240
+
251
241
  child.stdout.on('data', (chunk: Buffer) => {
252
242
  output = appendBounded(output, chunk, outputLimit);
253
243
  });
254
244
  child.stderr.on('data', (chunk: Buffer) => {
255
245
  output = appendBounded(output, chunk, outputLimit);
256
246
  });
247
+
248
+ const finish = async (code, signal, error?) => {
249
+ try {
250
+ await stopping;
251
+ } catch (cause) {
252
+ error = cause;
253
+ }
254
+
255
+ if (settled) return;
256
+ settled = true;
257
+ clearTimeout(timeout);
258
+ if (escalation) clearTimeout(escalation);
259
+
260
+ if (error) reject(error);
261
+ else resolve({ code, signal, output, timedOut });
262
+ };
263
+
257
264
  const timeout = setTimeout(() => {
258
265
  timedOut = true;
259
- child.kill('SIGTERM');
260
- killTimer = setTimeout(() => child.kill('SIGKILL'), 1_000);
261
- killTimer.unref?.();
266
+ stopping = Promise.resolve().then(() => terminateProcess(child, 'SIGTERM'));
267
+ if (process.platform === 'win32') {
268
+ void finish(null, 'SIGTERM');
269
+ } else {
270
+ stopping.catch((error) => {
271
+ void finish(null, null, error);
272
+ });
273
+ escalation = setTimeout(() => {
274
+ stopping = terminateProcess(child, 'SIGKILL');
275
+ stopping.catch((error) => {
276
+ void finish(null, null, error);
277
+ });
278
+ }, 1_000);
279
+ escalation.unref?.();
280
+ }
262
281
  }, timeoutMs);
263
282
  timeout.unref?.();
283
+
264
284
  child.on('error', (cause) => {
265
- if (settled) return;
266
- settled = true;
267
- clearTimeout(timeout);
268
- if (killTimer) clearTimeout(killTimer);
269
- reject(cause);
285
+ void finish(null, null, cause);
270
286
  });
271
287
  child.on('close', (code, signal) => {
272
- if (settled) return;
273
- settled = true;
274
- clearTimeout(timeout);
275
- if (killTimer) clearTimeout(killTimer);
276
- resolve({ code, signal, output, timedOut });
288
+ void finish(code, signal);
277
289
  });
278
290
  }).catch((cause) => {
279
291
  throw new HarmonyAutolinkingError(
@@ -307,6 +319,7 @@ async function materializeLocalSourceAsync(
307
319
  { packageName: module.packageName, stage: 'artifact-materialize' }
308
320
  );
309
321
  }
322
+
310
323
  const artifact = module.artifact;
311
324
  const projectRoot = await fs.promises.realpath(options.projectRoot);
312
325
  const packageRoot = await fs.promises.realpath(module.packageRoot);
@@ -324,6 +337,7 @@ async function materializeLocalSourceAsync(
324
337
  { packageName: module.packageName, stage: 'artifact-materialize' }
325
338
  );
326
339
  }
340
+
327
341
  const harPath = await resolveInsideAsync(
328
342
  packageRoot,
329
343
  module.arkTs.harPath,
@@ -334,12 +348,13 @@ async function materializeLocalSourceAsync(
334
348
  for (const step of fixedLocalSourceBuildSteps(artifact.build)) {
335
349
  await runFixedBuildStepAsync(step, module.packageName, options);
336
350
  }
337
- const outputReady = await isNonEmptyRegularHarAsync(
351
+
352
+ const ready = await isNonEmptyRegularHarAsync(
338
353
  artifact.outputPath,
339
354
  artifact.build.cwd,
340
355
  module.packageName
341
356
  );
342
- if (!outputReady) {
357
+ if (!ready) {
343
358
  throw new HarmonyAutolinkingError(
344
359
  'SOURCE_BUILD_FAILED',
345
360
  `Hvigor did not produce a non-empty HAR at ${normalizeSlashes(path.relative(packageRoot, artifact.outputPath))}.`,
@@ -359,6 +374,7 @@ async function materializeLocalSourceAsync(
359
374
  { packageName: module.packageName, stage: 'artifact-materialize' }
360
375
  );
361
376
  }
377
+
362
378
  return {
363
379
  packageName: module.packageName,
364
380
  harPath,
@@ -5,53 +5,83 @@ import { RnohCliPackage } from '../../config/constants';
5
5
  import { HarmonyAutolinkingError } from '../../errors';
6
6
  import { readJsonAsync, realpathExistingAsync, resolvePackageFromProject } from '../../utilities/values';
7
7
 
8
- function findPackageJson(projectRoot, packageName) {
8
+ function findPackageJson(root, packageName) {
9
9
  try {
10
- return resolvePackageFromProject(projectRoot, `${packageName}/package.json`);
10
+ return resolvePackageFromProject(root, `${packageName}/package.json`);
11
11
  } catch (cause) {
12
- throw new HarmonyAutolinkingError('RNOH_CLI_NOT_FOUND', `${RnohCliPackage} is not installed.`, {
12
+ throw new HarmonyAutolinkingError('RNOH_CLI_NOT_FOUND', `${packageName} is not installed.`, {
13
13
  cause,
14
14
  stage: 'rnoh-preflight',
15
15
  });
16
16
  }
17
17
  }
18
18
 
19
- async function resolveCliAsync(cliOptions) {
20
- const projectRoot = await realpathExistingAsync(cliOptions.projectRoot, {
19
+ async function resolveCliAsync(options) {
20
+ const root = await realpathExistingAsync(options.projectRoot, {
21
21
  type: 'directory',
22
22
  field: 'projectRoot',
23
23
  stage: 'rnoh-preflight',
24
24
  });
25
25
 
26
- const raw = cliOptions.rnohCliPackageJsonPath
27
- || findPackageJson(projectRoot, RnohCliPackage);
28
- const pkg = await realpathExistingAsync(raw, {
26
+ const raw = options.rnohCliPackageJsonPath
27
+ || findPackageJson(root, RnohCliPackage);
28
+ const manifest = await realpathExistingAsync(raw, {
29
29
  type: 'file',
30
30
  code: 'RNOH_CLI_NOT_FOUND',
31
31
  field: 'RNOH CLI package.json',
32
32
  stage: 'rnoh-preflight',
33
33
  });
34
34
 
35
- const packageJson = await readJsonAsync(pkg, 'RNOH_CLI_NOT_FOUND', 'rnoh-preflight');
36
- if (packageJson.name !== RnohCliPackage) {
37
- throw new HarmonyAutolinkingError('RNOH_CLI_NOT_FOUND', `Expected ${RnohCliPackage}, found ${String(packageJson.name)}.`, { stage: 'rnoh-preflight' });
35
+ const plugin = await readJsonAsync(manifest, 'RNOH_CLI_NOT_FOUND', 'rnoh-preflight');
36
+ if (plugin.name !== RnohCliPackage) {
37
+ throw new HarmonyAutolinkingError('RNOH_CLI_NOT_FOUND', `Expected ${RnohCliPackage}, found ${String(plugin.name)}.`, { stage: 'rnoh-preflight' });
38
38
  }
39
39
 
40
- const pluginRoot = path.dirname(pkg);
41
- const pluginConfig = path.join(pluginRoot, 'react-native.config.js');
42
- if (!fs.existsSync(pluginConfig)) {
40
+ const config = path.join(path.dirname(manifest), 'react-native.config.js');
41
+ if (!fs.existsSync(config)) {
43
42
  throw new HarmonyAutolinkingError('RNOH_CLI_NOT_FOUND', `${RnohCliPackage} does not expose its public React Native CLI plugin config.`, { stage: 'rnoh-preflight' });
44
43
  }
45
44
 
46
- const exec = cliOptions.reactNativeExecutable
47
- || path.join(cliOptions.nodeModulesPath || path.join(projectRoot, 'node_modules'), '.bin', 'react-native');
48
- const executable = await realpathExistingAsync(exec, {
45
+ if (options.reactNativeExecutable) {
46
+ const executable = await realpathExistingAsync(options.reactNativeExecutable, {
47
+ type: 'file',
48
+ code: 'RNOH_CLI_NOT_FOUND',
49
+ field: 'project-local react-native executable',
50
+ stage: 'rnoh-preflight',
51
+ });
52
+
53
+ return { executable, args: [] as string[] };
54
+ }
55
+
56
+ // Resolve the package itself, not a package-manager-generated .bin shell shim.
57
+ // The real manifest path also handles symlinked packages (for example pnpm).
58
+ const file = await realpathExistingAsync(
59
+ options.nodeModulesPath
60
+ ? path.join(options.nodeModulesPath, 'react-native', 'package.json')
61
+ : findPackageJson(root, 'react-native'),
62
+ {
63
+ type: 'file',
64
+ code: 'RNOH_CLI_NOT_FOUND',
65
+ field: 'project-local react-native package.json',
66
+ stage: 'rnoh-preflight',
67
+ }
68
+ );
69
+ const pkg = await readJsonAsync(file, 'RNOH_CLI_NOT_FOUND', 'rnoh-preflight');
70
+ const bin = typeof pkg.bin === 'string'
71
+ ? pkg.bin
72
+ : pkg.bin?.['react-native'];
73
+ if (typeof bin !== 'string' || !bin.trim()) {
74
+ throw new HarmonyAutolinkingError('RNOH_CLI_NOT_FOUND', 'The project-local react-native package does not declare a react-native bin entry.', { stage: 'rnoh-preflight' });
75
+ }
76
+
77
+ const entry = await realpathExistingAsync(path.resolve(path.dirname(file), bin), {
49
78
  type: 'file',
50
79
  code: 'RNOH_CLI_NOT_FOUND',
51
80
  field: 'project-local react-native executable',
52
81
  stage: 'rnoh-preflight',
53
82
  });
54
- return executable;
83
+
84
+ return { executable: process.execPath, args: [entry] };
55
85
  }
56
86
 
57
87
  export {
@@ -1,4 +1,4 @@
1
- import childProcess from 'node:child_process';
1
+ import crossSpawn from 'cross-spawn';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
 
@@ -6,6 +6,7 @@ import { RnohArtifacts } from '../../config/constants';
6
6
  import { HarmonyAutolinkingError } from '../../errors';
7
7
  import { assertSafeRnohPackageList } from '../../config/options';
8
8
  import { isPathInside, sanitizeOutput } from '../../utilities/values';
9
+ import { terminateProcess } from '../../utilities/terminateProcess';
9
10
  import { createManifest } from '../manifest/generate';
10
11
  import { canonicalizeOhpmManifest } from '../persistence/canonicalize';
11
12
  import { resolveRnohMetadata } from './packageMetadata';
@@ -36,14 +37,15 @@ function spawnBoundedAsync(executable, argv, options: Record<string, any> = {})
36
37
  let stdoutTruncated = false;
37
38
  let stderrTruncated = false;
38
39
  let timedOut = false;
39
- let killTimer;
40
+ let escalation;
40
41
  // eslint-disable-next-line prefer-const
41
42
  let timeout;
42
43
  let settled = false;
44
+ let stopping: Promise<void> | undefined;
43
45
  let child;
44
46
 
45
47
  try {
46
- child = childProcess.spawn(executable, argv, {
48
+ child = crossSpawn(executable, argv, {
47
49
  cwd: options.cwd,
48
50
  env: options.env || process.env,
49
51
  shell: false,
@@ -64,28 +66,20 @@ function spawnBoundedAsync(executable, argv, options: Record<string, any> = {})
64
66
  stderr = appendBounded(stderr, chunk, outputLimit);
65
67
  });
66
68
 
67
- child.on('error', (error) => {
68
- if (settled) return;
69
- settled = true;
70
- clearTimeout(timeout);
71
- clearTimeout(killTimer);
72
- reject(error);
73
- });
74
-
75
- timeout = setTimeout(() => {
76
- timedOut = true;
77
- child.kill('SIGTERM');
78
- killTimer = setTimeout(() => child.kill('SIGKILL'), 1_000);
79
- killTimer.unref?.();
80
- }, timeoutMs);
81
- timeout.unref?.();
69
+ const finish = async (code, signal, error?) => {
70
+ try {
71
+ await stopping;
72
+ } catch (cause) {
73
+ error = cause;
74
+ }
82
75
 
83
- child.on('close', (code, signal) => {
84
76
  if (settled) return;
85
77
  settled = true;
86
78
  clearTimeout(timeout);
87
- clearTimeout(killTimer);
88
- resolve({
79
+ clearTimeout(escalation);
80
+
81
+ if (error) reject(error);
82
+ else resolve({
89
83
  code,
90
84
  signal,
91
85
  timedOut,
@@ -94,6 +88,34 @@ function spawnBoundedAsync(executable, argv, options: Record<string, any> = {})
94
88
  stdoutTruncated,
95
89
  stderrTruncated,
96
90
  });
91
+ };
92
+
93
+ child.on('error', (error) => {
94
+ void finish(null, null, error);
95
+ });
96
+
97
+ timeout = setTimeout(() => {
98
+ timedOut = true;
99
+ stopping = Promise.resolve().then(() => terminateProcess(child, 'SIGTERM'));
100
+ if (process.platform === 'win32') {
101
+ void finish(null, 'SIGTERM');
102
+ } else {
103
+ stopping.catch((error) => {
104
+ void finish(null, null, error);
105
+ });
106
+ escalation = setTimeout(() => {
107
+ stopping = terminateProcess(child, 'SIGKILL');
108
+ stopping.catch((error) => {
109
+ void finish(null, null, error);
110
+ });
111
+ }, 1_000);
112
+ escalation.unref?.();
113
+ }
114
+ }, timeoutMs);
115
+ timeout.unref?.();
116
+
117
+ child.on('close', (code, signal) => {
118
+ void finish(code, signal);
97
119
  });
98
120
  });
99
121
  }
@@ -370,7 +392,7 @@ function patchOhpmManifest(artifact, descriptors, buildType, harmonyProjectPath)
370
392
  }
371
393
 
372
394
  async function linkRnohAsync(linkOptions) {
373
- const executable = await resolveCliAsync({
395
+ const command = await resolveCliAsync({
374
396
  ...linkOptions,
375
397
  nodeModulesPath: linkOptions.commandNodeModulesPath || linkOptions.nodeModulesPath,
376
398
  });
@@ -384,12 +406,12 @@ async function linkRnohAsync(linkOptions) {
384
406
 
385
407
  let result;
386
408
  try {
387
- result = await spawnBoundedAsync(executable, buildLinkCommandArgs({
409
+ result = await spawnBoundedAsync(command.executable, [...command.args, ...buildLinkCommandArgs({
388
410
  harmonyProjectPath: linkOptions.stageHarmonyProjectPath,
389
411
  nodeModulesPath: linkOptions.nodeModulesPath,
390
412
  include: linkOptions.include,
391
413
  exclude: linkOptions.exclude,
392
- }), spawn);
414
+ })], spawn);
393
415
  } catch (cause) {
394
416
  throw new HarmonyAutolinkingError('RNOH_LINK_FAILED', 'Unable to execute React Native link-harmony.', { cause, stage: 'rnoh-link' });
395
417
  }
@@ -0,0 +1,53 @@
1
+ import { spawn, type ChildProcess } from 'node:child_process';
2
+ import path from 'node:path';
3
+
4
+ /** Stop a Windows command and its descendants before callers restore build inputs. */
5
+ export async function terminateProcess(child: ChildProcess, signal: NodeJS.Signals): Promise<void> {
6
+ if (!child.pid) return;
7
+
8
+ if (process.platform !== 'win32') {
9
+ child.kill(signal);
10
+ return;
11
+ }
12
+
13
+ try {
14
+ await new Promise<void>((resolve, reject) => {
15
+ const command = path.win32.join(process.env.SystemRoot || process.env.SYSTEMROOT || 'C:\\Windows', 'System32', 'taskkill.exe');
16
+ const killer = spawn(command, ['/pid', String(child.pid), '/T', '/F'], {
17
+ shell: false,
18
+ windowsHide: true,
19
+ stdio: 'ignore',
20
+ });
21
+
22
+ const timer = setTimeout(() => {
23
+ try {
24
+ killer.kill('SIGKILL');
25
+ } catch { /* Still report the bounded termination failure. */ }
26
+ killer.unref();
27
+ reject(new Error(`Timed out terminating process tree ${child.pid}.`));
28
+ }, 5_000);
29
+
30
+ killer.once('error', (error) => {
31
+ clearTimeout(timer);
32
+ reject(error);
33
+ });
34
+ killer.once('exit', (code) => {
35
+ clearTimeout(timer);
36
+
37
+ if (code === 0) resolve();
38
+ else reject(new Error(`taskkill failed for process tree ${child.pid} (exit ${code}).`));
39
+ });
40
+ });
41
+ } catch (cause) {
42
+ // A failed taskkill must be reported, but must not keep the caller hung forever.
43
+ child.unref();
44
+
45
+ throw new Error(`Unable to terminate process tree ${child.pid}.`, { cause });
46
+ } finally {
47
+ // Descendants can retain pipe handles after the immediate child has exited.
48
+ // taskkill has finished (or failed explicitly); do not wait forever for EOF.
49
+ child.stdin?.destroy();
50
+ child.stdout?.destroy();
51
+ child.stderr?.destroy();
52
+ }
53
+ }
@@ -0,0 +1,14 @@
1
+ /** Keep tool overrides consistent across CLI, workspace and local HAR builds. */
2
+ export function resolveHarmonyCommand(command: string, args: readonly string[], env = process.env) {
3
+ const configured = command === 'ohpm'
4
+ ? env.HARMONY_OHPM
5
+ : command === 'hvigorw' ? env.HARMONY_HVIGORW : undefined;
6
+ if (!configured) return { command, args: [...args] };
7
+
8
+ const node = command === 'ohpm' ? env.HARMONY_OHPM_NODE : env.HARMONY_HVIGOR_NODE;
9
+ return /\.(?:c|m)?js$/iu.test(configured)
10
+ ? { command: node || env.HARMONY_NODE || process.execPath, args: [configured, ...args] }
11
+ : { command: configured, args: [...args] };
12
+ }
13
+
14
+ export { terminateProcess } from './terminateProcess';