@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 +6 -0
- package/build/src/harmony/materialize.js +56 -42
- package/build/src/harmony/rnoh/cli.d.ts +4 -1
- package/build/src/harmony/rnoh/cli.js +41 -18
- package/build/src/harmony/rnoh/link.js +52 -30
- package/build/src/utilities/terminateProcess.d.ts +3 -0
- package/build/src/utilities/terminateProcess.js +58 -0
- package/build/src/utilities/toolCommand.d.ts +6 -0
- package/build/src/utilities/toolCommand.js +18 -0
- package/package.json +7 -7
- package/scripts/build.mjs +27 -0
- package/src/harmony/materialize.ts +64 -48
- package/src/harmony/rnoh/cli.ts +48 -18
- package/src/harmony/rnoh/link.ts +46 -24
- package/src/utilities/terminateProcess.ts +53 -0
- package/src/utilities/toolCommand.ts +14 -0
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
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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
|
|
175
|
+
let stopping;
|
|
176
|
+
let escalation;
|
|
182
177
|
let child;
|
|
183
178
|
try {
|
|
184
|
-
child = (0,
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
256
|
-
if (!
|
|
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)({
|
|
@@ -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(
|
|
12
|
+
function findPackageJson(root, packageName) {
|
|
13
13
|
try {
|
|
14
|
-
return (0, values_1.resolvePackageFromProject)(
|
|
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', `${
|
|
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(
|
|
24
|
-
const
|
|
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 =
|
|
30
|
-
|| findPackageJson(
|
|
31
|
-
const
|
|
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
|
|
38
|
-
if (
|
|
39
|
-
throw new errors_1.HarmonyAutolinkingError('RNOH_CLI_NOT_FOUND', `Expected ${constants_1.RnohCliPackage}, found ${String(
|
|
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
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
73
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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,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.
|
|
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": "
|
|
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
|
|
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
|
-
|
|
121
|
+
throw new HarmonyAutolinkingError(
|
|
131
122
|
'SOURCE_ARTIFACT_NOT_PUBLISHED',
|
|
132
123
|
`${options.packageName} has a missing or empty Harmony HAR.`,
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
|
225
|
+
let stopping: Promise<void> | undefined;
|
|
226
|
+
let escalation: NodeJS.Timeout | undefined;
|
|
239
227
|
let child;
|
|
228
|
+
|
|
240
229
|
try {
|
|
241
|
-
child = spawn(
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
351
|
+
|
|
352
|
+
const ready = await isNonEmptyRegularHarAsync(
|
|
338
353
|
artifact.outputPath,
|
|
339
354
|
artifact.build.cwd,
|
|
340
355
|
module.packageName
|
|
341
356
|
);
|
|
342
|
-
if (!
|
|
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,
|
package/src/harmony/rnoh/cli.ts
CHANGED
|
@@ -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(
|
|
8
|
+
function findPackageJson(root, packageName) {
|
|
9
9
|
try {
|
|
10
|
-
return resolvePackageFromProject(
|
|
10
|
+
return resolvePackageFromProject(root, `${packageName}/package.json`);
|
|
11
11
|
} catch (cause) {
|
|
12
|
-
throw new HarmonyAutolinkingError('RNOH_CLI_NOT_FOUND', `${
|
|
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(
|
|
20
|
-
const
|
|
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 =
|
|
27
|
-
|| findPackageJson(
|
|
28
|
-
const
|
|
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
|
|
36
|
-
if (
|
|
37
|
-
throw new HarmonyAutolinkingError('RNOH_CLI_NOT_FOUND', `Expected ${RnohCliPackage}, found ${String(
|
|
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
|
|
41
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
83
|
+
|
|
84
|
+
return { executable: process.execPath, args: [entry] };
|
|
55
85
|
}
|
|
56
86
|
|
|
57
87
|
export {
|
package/src/harmony/rnoh/link.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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
|
|
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 =
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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(
|
|
88
|
-
|
|
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
|
|
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';
|