@expo-harmony/cli 55.0.26-harmony.1

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/src/tools.ts ADDED
@@ -0,0 +1,316 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import {
5
+ resolveHarmonyBuildPath,
6
+ type HarmonyBuildDescriptor,
7
+ } from '@expo-harmony/prebuild-config/build-descriptor';
8
+ import { readManifestIfPresentAsync } from '@expo-harmony/prebuild-config/check';
9
+
10
+ import { HarmonyCliError } from './errors';
11
+
12
+ export interface HarmonyTool {
13
+ args: string[];
14
+ command: string;
15
+ source: 'deveco' | 'override' | 'path';
16
+ }
17
+
18
+ export interface HarmonyToolchain {
19
+ hdc: HarmonyTool;
20
+ hvigor: HarmonyTool;
21
+ ohpm: HarmonyTool;
22
+ sdkHome: string | null;
23
+ toolsRoot: string | null;
24
+ }
25
+
26
+ export interface HarmonyBuildPlan {
27
+ abilityName: string;
28
+ buildMode: 'debug' | 'release';
29
+ bundleName: string;
30
+ expectedHap: string;
31
+ exportPaths: HarmonyBuildDescriptor['export'];
32
+ harmonyRoot: string;
33
+ hvigorArgs: string[];
34
+ moduleName: string;
35
+ moduleRoot: string;
36
+ nativeCache: HarmonyBuildDescriptor['nativeCache'];
37
+ nativeInputs: HarmonyBuildDescriptor['nativeInputs'];
38
+ projectFiles: HarmonyBuildDescriptor['projectFiles'];
39
+ productName: string;
40
+ targetName: string;
41
+ }
42
+
43
+ function existingFile(candidates: string[]): string | null {
44
+ return candidates.find(candidate => fs.existsSync(candidate)) || null;
45
+ }
46
+
47
+ const RequiredSdkComponents = Object.freeze([
48
+ 'default/sdk-pkg.json',
49
+ 'default/hms/ets/uni-package.json',
50
+ 'default/hms/native/uni-package.json',
51
+ 'default/hms/toolchains/uni-package.json',
52
+ 'default/openharmony/ets/oh-uni-package.json',
53
+ 'default/openharmony/native/oh-uni-package.json',
54
+ 'default/openharmony/toolchains/oh-uni-package.json',
55
+ ]);
56
+
57
+ function sdkRootsNear(seed) {
58
+ const roots = [];
59
+ let cursor = path.resolve(seed);
60
+
61
+ for (let depth = 0; depth < 6; depth += 1) {
62
+ const name = path.basename(cursor).toLowerCase();
63
+ if (name === 'default' && path.basename(path.dirname(cursor)).toLowerCase() === 'sdk') {
64
+ roots.push(path.dirname(cursor));
65
+ }
66
+ if (name === 'sdk') roots.push(cursor);
67
+ roots.push(path.join(cursor, 'sdk'));
68
+
69
+ const parent = path.dirname(cursor);
70
+ if (parent === cursor) break;
71
+ cursor = parent;
72
+ }
73
+
74
+ return roots;
75
+ }
76
+
77
+ function resolveHarmonySdkRoot(env, platform) {
78
+ const seeds = [
79
+ env.DEVECO_SDK_HOME,
80
+ env.HARMONY_HOME,
81
+ env.OHOS_SDK_HOME,
82
+ env.HARMONY_HVIGORW,
83
+ env.HARMONY_OHPM,
84
+ env.HARMONY_NODE,
85
+ ...(env.PATH || '').split(path.delimiter).filter(Boolean),
86
+ ...(platform === 'darwin' ? ['/Applications/DevEco-Studio.app/Contents'] : []),
87
+ ].filter(Boolean);
88
+ const candidates = [...new Set(seeds.flatMap(sdkRootsNear))];
89
+
90
+ return candidates.find(root => RequiredSdkComponents.every(
91
+ relative => fs.existsSync(path.join(root, ...relative.split('/')))
92
+ )) || null;
93
+ }
94
+
95
+ function devEcoInstallRoots(sdkHome) {
96
+ const ancestors = [sdkHome, path.dirname(sdkHome), path.dirname(path.dirname(sdkHome))];
97
+ const sdkDirectory = ancestors.find(candidate => path.basename(candidate).toLowerCase() === 'sdk');
98
+ const roots = [
99
+ sdkDirectory && path.dirname(sdkDirectory),
100
+ path.dirname(sdkHome),
101
+ path.dirname(path.dirname(sdkHome)),
102
+ ].filter(Boolean);
103
+
104
+ return [...new Set(roots)];
105
+ }
106
+
107
+ function devEcoLayouts(sdkHome) {
108
+ return devEcoInstallRoots(sdkHome).flatMap(root => [
109
+ {
110
+ nodeRoot: path.join(root, 'tools', 'node'),
111
+ toolsRoot: path.join(root, 'tools'),
112
+ },
113
+ {
114
+ nodeRoot: path.join(root, 'tool', 'node'),
115
+ toolsRoot: root,
116
+ },
117
+ ]);
118
+ }
119
+
120
+ function devEcoNode(layout, platform, env) {
121
+ if (env.HARMONY_NODE) return { command: env.HARMONY_NODE, source: 'override' };
122
+
123
+ const executable = platform === 'win32' ? 'node.exe' : 'node';
124
+ const command = existingFile([
125
+ path.join(layout.nodeRoot, 'bin', executable),
126
+ path.join(layout.nodeRoot, executable),
127
+ ]);
128
+
129
+ return command ? { command, source: 'deveco' } : null;
130
+ }
131
+
132
+ function resolveHarmonyToolchain(): HarmonyToolchain {
133
+ const env = process.env;
134
+ const platform = process.platform;
135
+
136
+ // Hvigor expects the SDK root containing default/, not default/ itself.
137
+ // Reject metadata-only candidates so doctor cannot claim an SDK is
138
+ // buildable without its HMS, OpenHarmony, native, ETS, and toolchain parts.
139
+ const sdkHome = resolveHarmonySdkRoot(env, platform);
140
+ const layouts = sdkHome ? devEcoLayouts(sdkHome) : [];
141
+
142
+ let ohpm: HarmonyTool;
143
+ if (env.HARMONY_OHPM) {
144
+ ohpm = { args: [], command: env.HARMONY_OHPM, source: 'override' };
145
+ } else {
146
+ const devEcoOhpm = existingFile(layouts.map(layout => path.join(
147
+ layout.toolsRoot,
148
+ 'ohpm',
149
+ 'bin',
150
+ platform === 'win32' ? 'ohpm.bat' : 'ohpm'
151
+ )));
152
+ ohpm = devEcoOhpm
153
+ ? { args: [], command: devEcoOhpm, source: 'deveco' }
154
+ : { args: [], command: platform === 'win32' ? 'ohpm.bat' : 'ohpm', source: 'path' };
155
+ }
156
+
157
+ let hvigor: HarmonyTool;
158
+ if (env.HARMONY_HVIGORW) {
159
+ if (/\.(?:c|m)?js$/iu.test(env.HARMONY_HVIGORW)) {
160
+ hvigor = {
161
+ args: [env.HARMONY_HVIGORW],
162
+ command: env.HARMONY_NODE || process.execPath,
163
+ source: 'override',
164
+ };
165
+ } else {
166
+ hvigor = { args: [], command: env.HARMONY_HVIGORW, source: 'override' };
167
+ }
168
+ } else {
169
+ const devEcoHvigor = layouts.map(layout => ({
170
+ layout,
171
+ node: devEcoNode(layout, platform, env),
172
+ script: path.join(layout.toolsRoot, 'hvigor', 'bin', 'hvigorw.js'),
173
+ })).find(candidate => candidate.node && fs.existsSync(candidate.script));
174
+ hvigor = devEcoHvigor
175
+ ? { args: [devEcoHvigor.script], command: devEcoHvigor.node.command, source: 'deveco' }
176
+ : { args: [], command: platform === 'win32' ? 'hvigorw.bat' : 'hvigorw', source: 'path' };
177
+ }
178
+
179
+ const toolsRoot = layouts.find(layout => (
180
+ hvigor.args[0]?.startsWith(`${layout.toolsRoot}${path.sep}`)
181
+ || ohpm.command.startsWith(`${layout.toolsRoot}${path.sep}`)
182
+ ))?.toolsRoot || null;
183
+
184
+ let hdc: HarmonyTool;
185
+ if (env.HARMONY_HDC) {
186
+ hdc = { args: [], command: env.HARMONY_HDC, source: 'override' };
187
+ } else {
188
+ const executable = platform === 'win32' ? 'hdc.exe' : 'hdc';
189
+ const sdkHdc = sdkHome && existingFile([
190
+ path.join(sdkHome, 'default', 'openharmony', 'toolchains', executable),
191
+ path.join(sdkHome, 'default', 'hms', 'toolchains', executable),
192
+ ]);
193
+ hdc = sdkHdc
194
+ ? { args: [], command: sdkHdc, source: 'deveco' }
195
+ : { args: [], command: executable, source: 'path' };
196
+ }
197
+
198
+ return { hdc, hvigor, ohpm, sdkHome, toolsRoot };
199
+ }
200
+
201
+ function createHarmonyToolchainEnv(toolchain = resolveHarmonyToolchain()): NodeJS.ProcessEnv {
202
+ return {
203
+ ...process.env,
204
+ HARMONY_OHPM: toolchain.ohpm.command,
205
+ // A script-based Hvigor invocation must retain both its script and Node executable.
206
+ HARMONY_HVIGORW: toolchain.hvigor.args[0] || toolchain.hvigor.command,
207
+ ...(toolchain.hvigor.args.length > 0 ? { HARMONY_NODE: toolchain.hvigor.command } : {}),
208
+ ...(toolchain.sdkHome && !process.env.DEVECO_SDK_HOME
209
+ ? { DEVECO_SDK_HOME: toolchain.sdkHome }
210
+ : {}),
211
+ };
212
+ }
213
+
214
+ function resolveHarmonyEmulator(toolchain: HarmonyToolchain): HarmonyTool {
215
+ if (process.env.HARMONY_EMULATOR) {
216
+ return { args: [], command: process.env.HARMONY_EMULATOR, source: 'override' };
217
+ }
218
+
219
+ const executable = process.platform === 'win32' ? 'Emulator.exe' : 'Emulator';
220
+ const roots = [
221
+ toolchain.toolsRoot,
222
+ ...(toolchain.sdkHome ? devEcoLayouts(toolchain.sdkHome).map(layout => layout.toolsRoot) : []),
223
+ ...(process.platform === 'darwin' ? ['/Applications/DevEco-Studio.app/Contents/tools'] : []),
224
+ ].filter(Boolean);
225
+ const command = existingFile(roots.map(root => path.join(root, 'emulator', executable)));
226
+
227
+ return command
228
+ ? { args: [], command, source: 'deveco' }
229
+ : { args: [], command: executable, source: 'path' };
230
+ }
231
+
232
+ function createHarmonyBuildPlan(
233
+ projectRoot: string,
234
+ build: HarmonyBuildDescriptor,
235
+ mode: 'debug' | 'release'
236
+ ): HarmonyBuildPlan {
237
+ const resolve = relative => resolveHarmonyBuildPath(projectRoot, relative);
238
+ const variant = build.variants[mode];
239
+
240
+ return {
241
+ abilityName: build.identity.abilityName,
242
+ buildMode: mode,
243
+ bundleName: build.identity.bundleName,
244
+ expectedHap: resolve(variant.expectedHap),
245
+ exportPaths: Object.fromEntries(
246
+ Object.entries(build.export).map(([name, relative]) => [name, resolve(relative)])
247
+ ) as HarmonyBuildPlan['exportPaths'],
248
+ harmonyRoot: resolve(build.harmonyRoot),
249
+ hvigorArgs: [...variant.hvigorArgs],
250
+ moduleName: build.identity.moduleName,
251
+ moduleRoot: resolve(build.moduleRoot),
252
+ nativeCache: {
253
+ invalidationRoots: build.nativeCache.invalidationRoots.map(resolve),
254
+ stateFile: resolve(build.nativeCache.stateFile),
255
+ },
256
+ nativeInputs: {
257
+ lockfile: resolve(build.nativeInputs.lockfile),
258
+ manifest: resolve(build.nativeInputs.manifest),
259
+ },
260
+ productName: build.identity.productName,
261
+ projectFiles: Object.fromEntries(
262
+ Object.entries(build.projectFiles).map(([name, relative]) => [name, resolve(relative)])
263
+ ) as HarmonyBuildPlan['projectFiles'],
264
+ targetName: build.identity.targetName,
265
+ };
266
+ }
267
+
268
+ async function resolveHarmonyBuildPlanIfPresentAsync(
269
+ projectRoot: string,
270
+ options: { buildMode?: 'debug' | 'release' } = {}
271
+ ): Promise<HarmonyBuildPlan | null> {
272
+ const mode = options.buildMode || 'debug';
273
+ if (!['debug', 'release'].includes(mode)) {
274
+ throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', `Harmony buildMode must be debug or release, received: ${mode}`, { operation: 'resolve-build' });
275
+ }
276
+
277
+ let manifest;
278
+
279
+ try {
280
+ manifest = await readManifestIfPresentAsync(projectRoot);
281
+ } catch (cause) {
282
+ throw new HarmonyCliError(
283
+ cause.code || 'ERR_HARMONY_TEMPLATE_INVALID',
284
+ `Cannot read the generated Harmony build descriptor: ${cause.message}`,
285
+ { cause, operation: 'resolve-build' }
286
+ );
287
+ }
288
+
289
+ if (!manifest) return null;
290
+
291
+ return createHarmonyBuildPlan(projectRoot, manifest.build, mode);
292
+ }
293
+
294
+ async function resolveHarmonyBuildPlanAsync(
295
+ projectRoot: string,
296
+ options: { buildMode?: 'debug' | 'release' } = {}
297
+ ): Promise<HarmonyBuildPlan> {
298
+ const plan = await resolveHarmonyBuildPlanIfPresentAsync(projectRoot, options);
299
+ if (!plan) {
300
+ throw new HarmonyCliError(
301
+ 'ERR_HARMONY_MANIFEST_DRIFT',
302
+ 'Cannot read the generated Harmony build descriptor because the CNG manifest is missing.',
303
+ { operation: 'resolve-build' }
304
+ );
305
+ }
306
+
307
+ return plan;
308
+ }
309
+
310
+ export {
311
+ createHarmonyToolchainEnv,
312
+ resolveHarmonyBuildPlanAsync,
313
+ resolveHarmonyBuildPlanIfPresentAsync,
314
+ resolveHarmonyEmulator,
315
+ resolveHarmonyToolchain,
316
+ };
@@ -0,0 +1,18 @@
1
+ const ProjectPackages = Object.freeze({
2
+ expo: 'expo',
3
+ expoAutolinking: '@expo-harmony/expo-modules-autolinking',
4
+ reactNative: 'react-native',
5
+ rnohCli: '@react-native-oh/react-native-harmony-cli',
6
+ rnohRuntime: '@react-native-oh/react-native-harmony',
7
+ });
8
+
9
+ const RequiredProjectPackages = Object.freeze([
10
+ ProjectPackages.rnohRuntime,
11
+ ProjectPackages.rnohCli,
12
+ ProjectPackages.expoAutolinking,
13
+ ]);
14
+
15
+ export {
16
+ ProjectPackages,
17
+ RequiredProjectPackages,
18
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "declaration": true,
4
+ "esModuleInterop": true,
5
+ "lib": ["ES2022"],
6
+ "module": "Node16",
7
+ "moduleResolution": "Node16",
8
+ "outDir": "build",
9
+ "rootDir": "src",
10
+ "skipLibCheck": true,
11
+ "strict": true,
12
+ "noImplicitAny": false,
13
+ "strictNullChecks": false,
14
+ "useUnknownInCatchVariables": false,
15
+ "target": "ES2022",
16
+ "types": ["node"]
17
+ },
18
+ "include": ["src/**/*.ts"],
19
+ "exclude": ["build"]
20
+ }