@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.
- package/README.md +74 -46
- package/build/bin/expo-harmony.js +1 -1
- package/build/buildHap/build.d.ts +3 -10
- package/build/buildHap/build.js +11 -69
- package/build/buildHap/index.d.ts +2 -0
- package/build/buildHap/index.js +19 -0
- package/build/cli.d.ts +3 -3
- package/build/cli.js +21 -187
- package/build/command.d.ts +9 -0
- package/build/command.js +20 -0
- package/build/doctor/doctor.d.ts +1 -1
- package/build/doctor/doctor.js +64 -56
- package/build/doctor/index.d.ts +2 -0
- package/build/doctor/index.js +16 -0
- package/build/exportEmbed/export.d.ts +2 -2
- package/build/exportEmbed/export.js +53 -49
- package/build/exportEmbed/index.d.ts +2 -0
- package/build/exportEmbed/index.js +19 -0
- package/build/file.d.ts +2 -1
- package/build/file.js +18 -8
- package/build/help.d.ts +1 -0
- package/build/help.js +36 -0
- package/build/index.d.ts +5 -3
- package/build/index.js +6 -5
- package/build/internal/prebuild.d.ts +3 -0
- package/build/internal/prebuild.js +9 -0
- package/build/log.d.ts +4 -0
- package/build/log.js +6 -0
- package/build/modules/index.d.ts +2 -0
- package/build/modules/index.js +16 -0
- package/build/native/bare.d.ts +4 -0
- package/build/native/bare.js +130 -0
- package/build/native/build.d.ts +15 -0
- package/build/native/build.js +67 -0
- package/build/{run → native}/cache.d.ts +2 -2
- package/build/{run → native}/cache.js +8 -8
- package/build/native/hvigor.d.ts +2 -0
- package/build/native/hvigor.js +77 -0
- package/build/native/install.d.ts +5 -0
- package/build/native/install.js +13 -0
- package/build/native/prepareProject.d.ts +11 -0
- package/build/native/prepareProject.js +68 -0
- package/build/native/project.d.ts +8 -0
- package/build/native/project.js +58 -0
- package/build/native/toolchain.d.ts +16 -0
- package/build/{tools.js → native/toolchain.js} +1 -57
- package/build/native/types.d.ts +47 -0
- package/build/native/types.js +2 -0
- package/build/prebuild/check.d.ts +7 -3
- package/build/prebuild/check.js +26 -20
- package/build/prebuild/clean.js +5 -5
- package/build/prebuild/index.d.ts +2 -0
- package/build/prebuild/index.js +51 -0
- package/build/prebuild/prebuild.js +11 -3
- package/build/prebuild/template.js +2 -2
- package/build/process.d.ts +11 -1
- package/build/process.js +16 -0
- package/build/profile.d.ts +1 -0
- package/build/profile.js +12 -0
- package/build/projectLock.js +7 -2
- package/build/run/devices.d.ts +1 -1
- package/build/run/devices.js +15 -10
- package/build/run/emulators.d.ts +1 -1
- package/build/run/emulators.js +2 -5
- package/build/run/index.d.ts +2 -0
- package/build/run/index.js +25 -0
- package/build/run/metro.js +1 -1
- package/build/run/run.d.ts +4 -10
- package/build/run/run.js +45 -90
- package/build/start/index.d.ts +2 -0
- package/build/start/index.js +33 -0
- package/package.json +14 -4
- package/src/bin/expo-harmony.ts +2 -2
- package/src/buildHap/build.ts +13 -105
- package/src/buildHap/index.ts +19 -0
- package/src/cli.ts +24 -227
- package/src/command.ts +24 -0
- package/src/doctor/doctor.ts +63 -61
- package/src/doctor/index.ts +16 -0
- package/src/exportEmbed/export.ts +64 -55
- package/src/exportEmbed/index.ts +17 -0
- package/src/file.ts +20 -11
- package/src/help.ts +33 -0
- package/src/index.ts +5 -3
- package/src/internal/prebuild.ts +3 -0
- package/src/log.ts +5 -0
- package/src/modules/index.ts +16 -0
- package/src/native/bare.ts +145 -0
- package/src/native/build.ts +81 -0
- package/src/{run → native}/cache.ts +10 -9
- package/src/native/hvigor.ts +81 -0
- package/src/native/install.ts +17 -0
- package/src/native/prepareProject.ts +99 -0
- package/src/native/project.ts +88 -0
- package/src/{tools.ts → native/toolchain.ts} +11 -118
- package/src/native/types.ts +44 -0
- package/src/prebuild/check.ts +30 -19
- package/src/prebuild/clean.ts +3 -3
- package/src/prebuild/index.ts +56 -0
- package/src/prebuild/prebuild.ts +11 -1
- package/src/prebuild/template.ts +2 -2
- package/src/process.ts +38 -1
- package/src/profile.ts +13 -0
- package/src/projectLock.ts +9 -1
- package/src/run/devices.ts +16 -11
- package/src/run/emulators.ts +4 -6
- package/src/run/index.ts +25 -0
- package/src/run/metro.ts +1 -1
- package/src/run/run.ts +56 -129
- package/src/start/index.ts +31 -0
- package/build/buildHap/common.d.ts +0 -24
- package/build/buildHap/common.js +0 -92
- package/build/run/install.d.ts +0 -7
- package/build/run/install.js +0 -26
- package/build/tools.d.ts +0 -39
- package/src/buildHap/common.ts +0 -145
- package/src/run/install.ts +0 -41
package/src/prebuild/check.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -5,20 +6,22 @@ import path from 'node:path';
|
|
|
5
6
|
import {
|
|
6
7
|
compareAsync,
|
|
7
8
|
stageAsync,
|
|
8
|
-
} from '@expo-harmony/prebuild-config/
|
|
9
|
+
} from '@expo-harmony/prebuild-config/internal';
|
|
9
10
|
|
|
10
11
|
import { HarmonyCliError } from '../errors';
|
|
11
12
|
import { isInside } from '../path';
|
|
12
13
|
import { spawnAsync } from '../process';
|
|
13
14
|
import { withHarmonyProjectLockAsync } from '../projectLock';
|
|
14
|
-
import { createHarmonyToolchainEnv
|
|
15
|
+
import { createHarmonyToolchainEnv } from '../native/toolchain';
|
|
16
|
+
import { resolveHarmonyBuildPlanAsync } from '../native/project';
|
|
17
|
+
import { type HarmonyBuildPlan } from '../native/types';
|
|
15
18
|
import { resolveExpoCli } from '../expo';
|
|
16
19
|
import { packAsync } from './template';
|
|
17
20
|
|
|
18
|
-
export type { Change as CheckChange } from '@expo-harmony/prebuild-config/
|
|
21
|
+
export type { Change as CheckChange } from '@expo-harmony/prebuild-config/internal';
|
|
19
22
|
|
|
20
|
-
const
|
|
21
|
-
const
|
|
23
|
+
const IgnoredDirectories = new Set(['.expo', '.git', '.hvigor', '.yarn', 'node_modules']);
|
|
24
|
+
const GeneratedDirectories = new Set([
|
|
22
25
|
'.cxx',
|
|
23
26
|
'.git',
|
|
24
27
|
'.hvigor',
|
|
@@ -103,16 +106,18 @@ function shouldCopyPrebuildCheckPath(
|
|
|
103
106
|
if (!relative) return true;
|
|
104
107
|
|
|
105
108
|
const segments = relative.split(path.sep);
|
|
106
|
-
if (
|
|
109
|
+
if (IgnoredDirectories.has(segments[0])) return false;
|
|
107
110
|
|
|
108
|
-
const
|
|
111
|
+
const root = isInside(plan.harmonyRoot, source)
|
|
109
112
|
? plan.harmonyRoot
|
|
110
113
|
: isAppLocalHarmonyPath(project, source)
|
|
111
114
|
? path.join(project, ...segments.slice(0, 3))
|
|
112
115
|
: null;
|
|
113
|
-
if (
|
|
114
|
-
const native = path.relative(
|
|
115
|
-
if (
|
|
116
|
+
if (root) {
|
|
117
|
+
const native = path.relative(root, source).split(path.sep);
|
|
118
|
+
if (GeneratedDirectories.has(native[0])
|
|
119
|
+
|| (GeneratedDirectories.has(native[1])
|
|
120
|
+
&& fs.existsSync(path.join(root, native[0], 'src/main/module.json5')))) return false;
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
return source !== plan.exportPaths.bundle;
|
|
@@ -126,6 +131,7 @@ async function copyAsync(
|
|
|
126
131
|
) {
|
|
127
132
|
await fs.promises.cp(project, target, {
|
|
128
133
|
recursive: true,
|
|
134
|
+
verbatimSymlinks: true,
|
|
129
135
|
filter: source => shouldCopyPrebuildCheckPath(project, source, plan),
|
|
130
136
|
});
|
|
131
137
|
|
|
@@ -137,16 +143,16 @@ async function copyAsync(
|
|
|
137
143
|
});
|
|
138
144
|
}
|
|
139
145
|
|
|
140
|
-
//
|
|
141
|
-
// entries. Dependency scanners then retain the isolated lexical package path
|
|
142
|
-
// (including scoped packages) instead of collapsing the entire node_modules
|
|
143
|
-
// root to the source project's realpath. Package directories stay linked;
|
|
144
|
-
// Windows copies loose files to avoid requiring file-symlink privileges.
|
|
146
|
+
// Link individual packages so dependency scanners retain paths inside the mirror.
|
|
145
147
|
await linkModulesAsync(modules, path.join(target, 'node_modules'));
|
|
146
148
|
await stageAsync(project, target, temp);
|
|
147
149
|
}
|
|
148
150
|
|
|
149
|
-
async function
|
|
151
|
+
async function withGeneratedProjectAsync<T>(
|
|
152
|
+
project: string,
|
|
153
|
+
options: CheckOptions & { clean?: boolean; skipPatches?: boolean },
|
|
154
|
+
action: (expected: string, checksum: string) => Promise<T>
|
|
155
|
+
): Promise<T> {
|
|
150
156
|
project = path.resolve(project);
|
|
151
157
|
const plan = await resolveHarmonyBuildPlanAsync(project);
|
|
152
158
|
const temp = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'expo-harmony-check-'));
|
|
@@ -155,6 +161,8 @@ async function checkUnlockedAsync(project, options: CheckOptions) {
|
|
|
155
161
|
|
|
156
162
|
try {
|
|
157
163
|
await copyAsync(project, expected, plan, temp);
|
|
164
|
+
if (options.clean) await fs.promises.rm(path.join(expected, 'harmony'), { recursive: true, force: true });
|
|
165
|
+
|
|
158
166
|
packed = await packAsync(project);
|
|
159
167
|
|
|
160
168
|
const expo = resolveExpoCli(project);
|
|
@@ -173,6 +181,7 @@ async function checkUnlockedAsync(project, options: CheckOptions) {
|
|
|
173
181
|
...packed.env,
|
|
174
182
|
...(options.buildType ? { EXPO_HARMONY_BUILD_TYPE: options.buildType } : {}),
|
|
175
183
|
EXPO_HARMONY_CHECK_MIRROR_ROOT: temp,
|
|
184
|
+
...(options.skipPatches ? { EXPO_HARMONY_SKIP_PATCHES: '1' } : {}),
|
|
176
185
|
},
|
|
177
186
|
operation: 'check-prebuild',
|
|
178
187
|
});
|
|
@@ -185,7 +194,9 @@ async function checkUnlockedAsync(project, options: CheckOptions) {
|
|
|
185
194
|
);
|
|
186
195
|
}
|
|
187
196
|
|
|
188
|
-
|
|
197
|
+
const checksum = createHash('md5').update(Uint8Array.from(await fs.promises.readFile(packed.tarball))).digest('hex');
|
|
198
|
+
|
|
199
|
+
return await action(expected, checksum);
|
|
189
200
|
} finally {
|
|
190
201
|
if (packed) await packed.cleanup();
|
|
191
202
|
await fs.promises.rm(temp, { recursive: true, force: true });
|
|
@@ -196,8 +207,8 @@ async function checkAsync(project, options: CheckOptions = {}) {
|
|
|
196
207
|
return withHarmonyProjectLockAsync(
|
|
197
208
|
project,
|
|
198
209
|
'prebuild-check',
|
|
199
|
-
() =>
|
|
210
|
+
() => withGeneratedProjectAsync(project, options, expected => compareAsync(project, expected))
|
|
200
211
|
);
|
|
201
212
|
}
|
|
202
213
|
|
|
203
|
-
export { checkAsync };
|
|
214
|
+
export { checkAsync, withGeneratedProjectAsync };
|
package/src/prebuild/clean.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
import { HarmonyPlatformDirectory } from '@expo-harmony/prebuild-config/
|
|
4
|
+
import { HarmonyPlatformDirectory } from '@expo-harmony/prebuild-config/internal';
|
|
5
5
|
|
|
6
6
|
import { HarmonyCliError } from '../errors';
|
|
7
7
|
import { isInside } from '../path';
|
|
8
|
-
import { resolveHarmonyBuildPlanIfPresentAsync } from '../
|
|
8
|
+
import { resolveHarmonyBuildPlanIfPresentAsync } from '../native/project';
|
|
9
9
|
|
|
10
10
|
async function assertSafeCleanTarget(projectRoot) {
|
|
11
11
|
const root = await fs.promises.realpath(projectRoot);
|
|
@@ -24,7 +24,7 @@ async function assertSafeCleanTarget(projectRoot) {
|
|
|
24
24
|
);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
if (!plan) {
|
|
27
|
+
if (!plan || plan.workflow !== 'cng') {
|
|
28
28
|
throw new HarmonyCliError(
|
|
29
29
|
'ERR_HARMONY_CLEAN_TARGET',
|
|
30
30
|
`Refusing to clean ${target} because its CNG manifest is missing. Please delete it manually or run prebuild without --clean.`,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { parseCommandArgs, type Command } from '../command';
|
|
2
|
+
import { HarmonyCliError } from '../errors';
|
|
3
|
+
import { withHarmonyProjectLockAsync } from '../projectLock';
|
|
4
|
+
import { parsePrebuildArgs } from './options';
|
|
5
|
+
|
|
6
|
+
export const command: Command = async (argv, io) => {
|
|
7
|
+
const args = parseCommandArgs(args => parsePrebuildArgs(args, { allowProject: true }), argv, io);
|
|
8
|
+
if (!args) return 0;
|
|
9
|
+
|
|
10
|
+
const { projectRoot: root, options } = args;
|
|
11
|
+
const { check, clean, passthrough } = options;
|
|
12
|
+
|
|
13
|
+
if (check && passthrough.length) {
|
|
14
|
+
throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--check cannot be combined with mutating prebuild options.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return withHarmonyProjectLockAsync(
|
|
18
|
+
root,
|
|
19
|
+
check ? 'prebuild-check' : 'prebuild',
|
|
20
|
+
async () => {
|
|
21
|
+
if (clean) {
|
|
22
|
+
const { assertSafeCleanTarget } = await import('./clean.js');
|
|
23
|
+
await assertSafeCleanTarget(root);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { doctorAsync, formatDoctor } = await import('../doctor/doctor.js');
|
|
27
|
+
const doctor = await doctorAsync(root, {
|
|
28
|
+
requireBuildTools: false,
|
|
29
|
+
validateGeneratedProject: !clean,
|
|
30
|
+
validateModules: false,
|
|
31
|
+
});
|
|
32
|
+
if (!doctor.ok) {
|
|
33
|
+
io.error(formatDoctor(doctor));
|
|
34
|
+
throw new HarmonyCliError('ERR_HARMONY_DOCTOR_FAILED', 'Harmony doctor found blocking errors.', {
|
|
35
|
+
operation: 'doctor',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const item of doctor.checks.filter(item => item.status === 'warn')) io.warn(`! ${item.message}`);
|
|
40
|
+
|
|
41
|
+
if (check) {
|
|
42
|
+
const { checkAsync } = await import('./check.js');
|
|
43
|
+
const result = await checkAsync(root);
|
|
44
|
+
|
|
45
|
+
if (result.clean) io.log('Harmony CNG output is up to date.');
|
|
46
|
+
else for (const change of result.changes) io.log(`${change.type}: ${change.path}`);
|
|
47
|
+
|
|
48
|
+
return result.clean ? 0 : 2;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const { prebuildParsedAsync } = await import('./prebuild.js');
|
|
52
|
+
await prebuildParsedAsync(root, passthrough);
|
|
53
|
+
|
|
54
|
+
return 0;
|
|
55
|
+
});
|
|
56
|
+
};
|
package/src/prebuild/prebuild.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
|
|
3
3
|
import { HarmonyCliError } from '../errors';
|
|
4
|
+
import { isBareHarmonyProject } from '../native/bare';
|
|
4
5
|
import { formatDiagnostics, spawnAsync } from '../process';
|
|
5
6
|
import { withHarmonyProjectLockAsync } from '../projectLock';
|
|
6
7
|
import { resolveExpoCli } from '../expo';
|
|
7
|
-
import { createHarmonyToolchainEnv
|
|
8
|
+
import { createHarmonyToolchainEnv } from '../native/toolchain';
|
|
9
|
+
import { resolveHarmonyBuildPlanAsync } from '../native/project';
|
|
8
10
|
import { assertSafeCleanTarget } from './clean';
|
|
9
11
|
import { packAsync } from './template';
|
|
10
12
|
|
|
@@ -18,6 +20,9 @@ async function prebuildParsedUnlockedAsync(
|
|
|
18
20
|
passthrough: string[],
|
|
19
21
|
options: PrebuildOptions = {}
|
|
20
22
|
) {
|
|
23
|
+
if (isBareHarmonyProject(projectRoot)) {
|
|
24
|
+
throw new HarmonyCliError('ERR_HARMONY_BARE_PREBUILD', 'This Harmony project is manually maintained. Build it directly; prebuild cannot overwrite a bare project.', { operation: 'prebuild' });
|
|
25
|
+
}
|
|
21
26
|
if (passthrough.includes('--clean')) await assertSafeCleanTarget(projectRoot);
|
|
22
27
|
|
|
23
28
|
const expo = resolveExpoCli(projectRoot);
|
|
@@ -44,6 +49,7 @@ async function prebuildParsedUnlockedAsync(
|
|
|
44
49
|
|
|
45
50
|
if (result.code !== 0) {
|
|
46
51
|
const diagnostics = options.capture ? formatDiagnostics(result) : '';
|
|
52
|
+
|
|
47
53
|
throw new HarmonyCliError('ERR_HARMONY_PREBUILD_FAILED', `Expo prebuild exited with code ${result.code}.${diagnostics ? `\n${diagnostics}` : ''}`, {
|
|
48
54
|
exitCode: result.code,
|
|
49
55
|
operation: 'expo-prebuild',
|
|
@@ -54,6 +60,10 @@ async function prebuildParsedUnlockedAsync(
|
|
|
54
60
|
const plan = await resolveHarmonyBuildPlanAsync(projectRoot, {
|
|
55
61
|
buildMode: options.buildType,
|
|
56
62
|
});
|
|
63
|
+
if (plan.workflow !== 'cng') {
|
|
64
|
+
throw new HarmonyCliError('ERR_HARMONY_TEMPLATE_INVALID', 'Prebuild did not produce a CNG project.', { operation: 'prebuild' });
|
|
65
|
+
}
|
|
66
|
+
|
|
57
67
|
await fs.promises.access(plan.projectFiles.templateMarker, fs.constants.R_OK);
|
|
58
68
|
} catch (cause) {
|
|
59
69
|
throw new HarmonyCliError('ERR_HARMONY_TEMPLATE_INVALID', 'Expo prebuild exited successfully but the Harmony marker or CNG manifest is invalid.', { cause, operation: 'verify-prebuild' });
|
package/src/prebuild/template.ts
CHANGED
|
@@ -19,11 +19,11 @@ function resolve(project: string) {
|
|
|
19
19
|
let api: Api;
|
|
20
20
|
|
|
21
21
|
try {
|
|
22
|
-
api = load('@expo-harmony/prebuild-config/template');
|
|
22
|
+
api = load('@expo-harmony/prebuild-config/internal/template');
|
|
23
23
|
} catch (cause) {
|
|
24
24
|
throw new HarmonyCliError(
|
|
25
25
|
'ERR_HARMONY_TEMPLATE_INVALID',
|
|
26
|
-
'Cannot load @expo-harmony/prebuild-config/template from the project. Update the project-local prebuild config to a compatible version.',
|
|
26
|
+
'Cannot load @expo-harmony/prebuild-config/internal/template from the project. Update the project-local prebuild config to a compatible version.',
|
|
27
27
|
{ cause, operation: 'resolve-template' }
|
|
28
28
|
);
|
|
29
29
|
}
|
package/src/process.ts
CHANGED
|
@@ -28,6 +28,16 @@ export interface ProcessResult {
|
|
|
28
28
|
timedOut: boolean;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
interface CheckedProcessOptions {
|
|
32
|
+
code: string;
|
|
33
|
+
cwd: string;
|
|
34
|
+
env?: NodeJS.ProcessEnv;
|
|
35
|
+
message: string;
|
|
36
|
+
operation: string;
|
|
37
|
+
outputLimit?: number;
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
class BoundedCapture {
|
|
32
42
|
private buffers: Uint8Array[] = [];
|
|
33
43
|
private head = 0;
|
|
@@ -346,4 +356,31 @@ function startManagedProcess(command: string, args: string[], options: ProcessOp
|
|
|
346
356
|
};
|
|
347
357
|
}
|
|
348
358
|
|
|
349
|
-
|
|
359
|
+
async function runCheckedAsync(
|
|
360
|
+
command: string,
|
|
361
|
+
args: string[],
|
|
362
|
+
options: CheckedProcessOptions
|
|
363
|
+
) {
|
|
364
|
+
const result = await spawnAsync(command, args, {
|
|
365
|
+
capture: true,
|
|
366
|
+
cwd: options.cwd,
|
|
367
|
+
env: options.env,
|
|
368
|
+
operation: options.operation,
|
|
369
|
+
outputLimit: options.outputLimit || 2 * 1024 * 1024,
|
|
370
|
+
timeoutMs: options.timeoutMs,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
if (result.code !== 0 || result.timedOut) {
|
|
374
|
+
const diagnostics = formatDiagnostics(result);
|
|
375
|
+
|
|
376
|
+
throw new HarmonyCliError(
|
|
377
|
+
options.code,
|
|
378
|
+
`${options.message} exited with code ${result.code}${result.timedOut ? ' after timing out' : ''}.${diagnostics ? `\n${diagnostics}` : ''}`,
|
|
379
|
+
{ exitCode: result.code || 1, operation: options.operation }
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export { formatDiagnostics, runCheckedAsync, spawnAsync, startManagedProcess };
|
package/src/profile.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export async function timedAsync<T>(
|
|
2
|
+
steps: Record<string, number>,
|
|
3
|
+
name: string,
|
|
4
|
+
operation: () => Promise<T> | T
|
|
5
|
+
): Promise<T> {
|
|
6
|
+
const started = performance.now();
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
return await operation();
|
|
10
|
+
} finally {
|
|
11
|
+
steps[name] = Math.max(0, Math.round(performance.now() - started));
|
|
12
|
+
}
|
|
13
|
+
}
|
package/src/projectLock.ts
CHANGED
|
@@ -162,6 +162,7 @@ async function acquireHarmonyProjectLockAsync(
|
|
|
162
162
|
await handle.sync();
|
|
163
163
|
} catch (cause) {
|
|
164
164
|
await releaseOwnedLockAsync(handle, lockPath, owned);
|
|
165
|
+
|
|
165
166
|
throw new HarmonyCliError(
|
|
166
167
|
'ERR_HARMONY_PROJECT_LOCK',
|
|
167
168
|
'Cannot initialize the Harmony native operation lock.',
|
|
@@ -183,7 +184,14 @@ async function acquireHarmonyProjectLockAsync(
|
|
|
183
184
|
},
|
|
184
185
|
};
|
|
185
186
|
} catch (cause) {
|
|
186
|
-
if (cause instanceof HarmonyCliError)
|
|
187
|
+
if (cause instanceof HarmonyCliError) {
|
|
188
|
+
throw new HarmonyCliError(cause.code, cause.message, {
|
|
189
|
+
cause,
|
|
190
|
+
exitCode: cause.exitCode,
|
|
191
|
+
operation: cause.operation,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
187
195
|
if (cause?.code !== 'EEXIST') {
|
|
188
196
|
throw new HarmonyCliError(
|
|
189
197
|
'ERR_HARMONY_PROJECT_LOCK',
|
package/src/run/devices.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { setTimeout as delay } from 'node:timers/promises';
|
|
|
2
2
|
|
|
3
3
|
import { listEmulatorsAsync, startEmulator } from './emulators';
|
|
4
4
|
import { HarmonyCliError } from '../errors';
|
|
5
|
-
import type
|
|
5
|
+
import { type HarmonyTool } from '../native/toolchain';
|
|
6
6
|
import { formatDiagnostics, spawnAsync, type ProcessResult } from '../process';
|
|
7
7
|
|
|
8
8
|
interface Device {
|
|
@@ -44,8 +44,6 @@ function parseHdcTargets(output: string): Device[] {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
return {
|
|
47
|
-
// Only the first HDC column is a selectable target name. The remaining
|
|
48
|
-
// verbose columns describe transport, state, location and connect tool.
|
|
49
47
|
aliases: [fields[0]],
|
|
50
48
|
connectTool: fields[4] || null,
|
|
51
49
|
id: fields[0],
|
|
@@ -59,10 +57,7 @@ function parseHdcTargets(output: string): Device[] {
|
|
|
59
57
|
function hasCommandFailure(result: ProcessResult): boolean {
|
|
60
58
|
const output = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
61
59
|
return result.code !== 0 || result.timedOut
|
|
62
|
-
// HDC
|
|
63
|
-
// stdout while still returning exit code 0 (for example,
|
|
64
|
-
// "Connect server failed"). Treat its documented failure vocabulary as
|
|
65
|
-
// authoritative regardless of where it appears on the line.
|
|
60
|
+
// HDC can print failures to stdout while returning exit code 0.
|
|
66
61
|
|| /\[(?:Fail|Error)\]|\b(?:Failure|failed)\b|(?:失败|错误)/iu.test(output);
|
|
67
62
|
}
|
|
68
63
|
|
|
@@ -146,7 +141,6 @@ async function selectDeviceAsync(
|
|
|
146
141
|
}
|
|
147
142
|
|
|
148
143
|
const instances = await listEmulatorsAsync(options.emulator, { cwd: options.cwd });
|
|
149
|
-
// Prefer an instance that is already booting when HDC is not connected yet.
|
|
150
144
|
const running = instances.filter(instance => instance.running);
|
|
151
145
|
const candidates = requested
|
|
152
146
|
? instances.filter(instance => instance.name === requested)
|
|
@@ -242,13 +236,24 @@ async function configureMetroPortAsync(
|
|
|
242
236
|
const deviceEndpoint = `tcp:${options.devicePort || 8081}`;
|
|
243
237
|
const hostEndpoint = `tcp:${port}`;
|
|
244
238
|
|
|
245
|
-
await runHdcAsync(hdc, ['-t', device.id, 'fport', '
|
|
246
|
-
allowFailure: true,
|
|
239
|
+
const forwards = await runHdcAsync(hdc, ['-t', device.id, 'fport', 'ls'], {
|
|
247
240
|
cwd: options.cwd,
|
|
248
|
-
operation: '
|
|
241
|
+
operation: 'list-metro-ports',
|
|
249
242
|
timeoutMs: 15_000,
|
|
250
243
|
});
|
|
251
244
|
|
|
245
|
+
for (const line of forwards.stdout.split(/\r?\n/u)) {
|
|
246
|
+
const [target, remote, local, direction] = line.trim().split(/\s+/u);
|
|
247
|
+
if (target !== device.id || remote !== deviceEndpoint || direction !== '[Reverse]') continue;
|
|
248
|
+
if (local === hostEndpoint) return;
|
|
249
|
+
|
|
250
|
+
await runHdcAsync(hdc, ['-t', device.id, 'fport', 'rm', remote, local], {
|
|
251
|
+
cwd: options.cwd,
|
|
252
|
+
operation: 'remove-metro-port',
|
|
253
|
+
timeoutMs: 15_000,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
252
257
|
await runHdcAsync(hdc, ['-t', device.id, 'rport', deviceEndpoint, hostEndpoint], {
|
|
253
258
|
code: 'ERR_HARMONY_METRO_FORWARD',
|
|
254
259
|
cwd: options.cwd,
|
package/src/run/emulators.ts
CHANGED
|
@@ -4,7 +4,7 @@ import spawn from 'cross-spawn';
|
|
|
4
4
|
|
|
5
5
|
import { HarmonyCliError } from '../errors';
|
|
6
6
|
import { formatDiagnostics, spawnAsync } from '../process';
|
|
7
|
-
import type
|
|
7
|
+
import { type HarmonyTool } from '../native/toolchain';
|
|
8
8
|
|
|
9
9
|
interface HarmonyEmulator {
|
|
10
10
|
name: string;
|
|
@@ -67,10 +67,9 @@ function startEmulator(tool: HarmonyTool, name: string, logFile: string, cwd?: s
|
|
|
67
67
|
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
68
68
|
const log = fs.openSync(logFile, 'w', 0o600);
|
|
69
69
|
let failure: string | null = null;
|
|
70
|
+
|
|
70
71
|
try {
|
|
71
|
-
//
|
|
72
|
-
// process group and file-backed output so it survives the CLI/Metro exit.
|
|
73
|
-
// Default instance/image paths match the ones used by -list -details.
|
|
72
|
+
// A detached process with file output lets the emulator survive CLI exit.
|
|
74
73
|
const child = spawn(tool.command, [...tool.args, '-start', name], {
|
|
75
74
|
cwd,
|
|
76
75
|
detached: true,
|
|
@@ -82,8 +81,7 @@ function startEmulator(tool: HarmonyTool, name: string, logFile: string, cwd?: s
|
|
|
82
81
|
failure = cause.message;
|
|
83
82
|
});
|
|
84
83
|
child.once('exit', (code, signal) => {
|
|
85
|
-
// Some
|
|
86
|
-
// prove readiness; the caller still checks the guest's name and boot state.
|
|
84
|
+
// Some launchers exit before the guest boots; HDC still has to confirm readiness.
|
|
87
85
|
if (code !== 0) failure = signal ? `terminated by ${signal}` : `exited with code ${code}`;
|
|
88
86
|
});
|
|
89
87
|
child.unref();
|
package/src/run/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { parseCommandArgs, type Command } from '../command';
|
|
2
|
+
import { parseRunArgs } from './options';
|
|
3
|
+
|
|
4
|
+
export const command: Command = async (argv, io) => {
|
|
5
|
+
const args = parseCommandArgs(parseRunArgs, argv, io);
|
|
6
|
+
if (!args) return 0;
|
|
7
|
+
|
|
8
|
+
const { projectRoot: root, options } = args;
|
|
9
|
+
const { runHarmonySessionAsync } = await import('./run.js');
|
|
10
|
+
|
|
11
|
+
const session = await runHarmonySessionAsync(root, {
|
|
12
|
+
...options,
|
|
13
|
+
interactiveBundler: true,
|
|
14
|
+
io,
|
|
15
|
+
});
|
|
16
|
+
const { result } = session;
|
|
17
|
+
|
|
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
|
+
|
|
24
|
+
return 0;
|
|
25
|
+
};
|
package/src/run/metro.ts
CHANGED
|
@@ -147,7 +147,7 @@ async function startExpoMetroAsync(
|
|
|
147
147
|
);
|
|
148
148
|
|
|
149
149
|
const startedAt = Date.now();
|
|
150
|
-
const timeoutMs = options.readyTimeoutMs ||
|
|
150
|
+
const timeoutMs = options.readyTimeoutMs || 1800000;
|
|
151
151
|
|
|
152
152
|
try {
|
|
153
153
|
while (Date.now() - startedAt < timeoutMs) {
|