@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
|
@@ -3,11 +3,11 @@ import { createRequire } from 'node:module';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
|
-
import { doctorAsync
|
|
6
|
+
import { doctorAsync } from '../doctor/doctor';
|
|
7
7
|
import { resolveHarmonyEntryPoint } from '../entry';
|
|
8
8
|
import { HarmonyCliError } from '../errors';
|
|
9
9
|
import { withHarmonyProjectLockAsync } from '../projectLock';
|
|
10
|
-
import { resolveHarmonyBuildPlanAsync } from '../
|
|
10
|
+
import { resolveHarmonyBuildPlanAsync } from '../native/project';
|
|
11
11
|
import { formatDiagnostics, spawnAsync } from '../process';
|
|
12
12
|
import { resolveExpoCli, resolveExpoHermesBuilder } from '../expo';
|
|
13
13
|
import {
|
|
@@ -32,65 +32,65 @@ export interface ExportTemporary {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function createExpoExportEmbedArgs(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
root: string,
|
|
36
|
+
entry: string,
|
|
37
|
+
temp: ExportTemporary,
|
|
38
38
|
options: ExportOptions = {}
|
|
39
39
|
) {
|
|
40
40
|
return [
|
|
41
41
|
'export:embed',
|
|
42
42
|
'--platform', 'harmony',
|
|
43
|
-
'--entry-file',
|
|
44
|
-
'--bundle-output',
|
|
45
|
-
'--assets-dest',
|
|
43
|
+
'--entry-file', entry,
|
|
44
|
+
'--bundle-output', temp.javascript,
|
|
45
|
+
'--assets-dest', temp.assets,
|
|
46
46
|
'--dev', 'false',
|
|
47
47
|
'--minify', 'false',
|
|
48
|
-
'--sourcemap-output',
|
|
48
|
+
'--sourcemap-output', temp.metroSourceMap,
|
|
49
49
|
'--sourcemap-sources-root', '.',
|
|
50
50
|
'--unstable-transform-profile', 'hermes-stable',
|
|
51
51
|
...(options.resetCache ? ['--reset-cache=true'] : []),
|
|
52
|
-
|
|
52
|
+
root,
|
|
53
53
|
];
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function assertDoctor(result: DoctorResult) {
|
|
57
|
-
if (result.ok) return;
|
|
58
|
-
|
|
59
|
-
const failing = result.checks.filter(check => check.status === 'error').map(check => check.id);
|
|
60
|
-
throw new HarmonyCliError('ERR_HARMONY_EXPORT_DOCTOR', `Harmony doctor found blocking checks: ${failing.join(', ') || 'unknown'}.`, { operation: 'doctor' });
|
|
61
|
-
}
|
|
62
|
-
|
|
63
56
|
async function exportEmbedUnlockedAsync(
|
|
64
|
-
|
|
57
|
+
root: string,
|
|
65
58
|
options: ExportOptions = {}
|
|
66
59
|
): Promise<HarmonyExportManifest> {
|
|
67
|
-
if (!options.skipDoctor)
|
|
60
|
+
if (!options.skipDoctor) {
|
|
61
|
+
const doctor = await doctorAsync(root);
|
|
62
|
+
if (!doctor.ok) {
|
|
63
|
+
const checks = doctor.checks.filter(check => check.status === 'error').map(check => check.id);
|
|
64
|
+
|
|
65
|
+
throw new HarmonyCliError('ERR_HARMONY_EXPORT_DOCTOR', `Harmony doctor found blocking checks: ${checks.join(', ') || 'unknown'}.`, { operation: 'doctor' });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
68
|
|
|
69
|
-
const plan = await resolveHarmonyBuildPlanAsync(
|
|
69
|
+
const plan = await resolveHarmonyBuildPlanAsync(root, { buildMode: 'release' });
|
|
70
70
|
const paths = exportPaths(plan);
|
|
71
71
|
|
|
72
72
|
if (options.check) return await validatePublishedExportAsync(paths);
|
|
73
73
|
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
assets: path.join(
|
|
78
|
-
bundle: path.join(
|
|
79
|
-
javascript: path.join(
|
|
80
|
-
metroSourceMap: path.join(
|
|
81
|
-
sourceMap: path.join(
|
|
74
|
+
const entry = resolveHarmonyEntryPoint(root);
|
|
75
|
+
const directory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'expo-harmony-export-'));
|
|
76
|
+
const temp: ExportTemporary = {
|
|
77
|
+
assets: path.join(directory, 'assets'),
|
|
78
|
+
bundle: path.join(directory, 'hermes_bundle.hbc'),
|
|
79
|
+
javascript: path.join(directory, 'index.js'),
|
|
80
|
+
metroSourceMap: path.join(directory, 'index.js.map'),
|
|
81
|
+
sourceMap: path.join(directory, 'hermes_bundle.hbc.map'),
|
|
82
82
|
};
|
|
83
83
|
|
|
84
84
|
try {
|
|
85
|
-
await fs.promises.mkdir(
|
|
85
|
+
await fs.promises.mkdir(temp.assets, { recursive: true });
|
|
86
86
|
|
|
87
|
-
const expo = resolveExpoCli(
|
|
87
|
+
const expo = resolveExpoCli(root);
|
|
88
88
|
const result = await spawnAsync(process.execPath, [
|
|
89
89
|
expo.cliPath,
|
|
90
|
-
...createExpoExportEmbedArgs(
|
|
90
|
+
...createExpoExportEmbedArgs(root, entry, temp, options),
|
|
91
91
|
], {
|
|
92
92
|
capture: true,
|
|
93
|
-
cwd:
|
|
93
|
+
cwd: root,
|
|
94
94
|
env: {
|
|
95
95
|
...process.env,
|
|
96
96
|
EXPO_METRO_TARGET: 'harmony',
|
|
@@ -104,6 +104,7 @@ async function exportEmbedUnlockedAsync(
|
|
|
104
104
|
|
|
105
105
|
if (result.code !== 0 || result.timedOut) {
|
|
106
106
|
const diagnostics = formatDiagnostics(result);
|
|
107
|
+
|
|
107
108
|
throw new HarmonyCliError(
|
|
108
109
|
'ERR_HARMONY_EXPORT_FAILED',
|
|
109
110
|
`Expo export:embed exited with code ${result.code}${result.timedOut ? ' after timing out' : ''}.${diagnostics ? `\n${diagnostics}` : ''}`,
|
|
@@ -115,24 +116,27 @@ async function exportEmbedUnlockedAsync(
|
|
|
115
116
|
// then explicitly hand its JS and source map to Expo's own Hermes exporter.
|
|
116
117
|
try {
|
|
117
118
|
const [code, map] = await Promise.all([
|
|
118
|
-
fs.promises.readFile(
|
|
119
|
-
fs.promises.readFile(
|
|
119
|
+
fs.promises.readFile(temp.javascript, 'utf8'),
|
|
120
|
+
fs.promises.readFile(temp.metroSourceMap, 'utf8'),
|
|
120
121
|
]);
|
|
121
|
-
const buildHermesBundleAsync = resolveExpoHermesBuilder(
|
|
122
|
+
const buildHermesBundleAsync = resolveExpoHermesBuilder(root);
|
|
123
|
+
|
|
122
124
|
// Expo resolves hermes-compiler from react-native. For Harmony, resolve it
|
|
123
125
|
// from RNOH instead of the app's Android/iOS React Native installation.
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
await fs.promises.
|
|
126
|
+
const require = createRequire(path.join(root, 'package.json'));
|
|
127
|
+
const runtime = path.dirname(require.resolve('@react-native-oh/react-native-harmony/package.json'));
|
|
128
|
+
const modules = path.join(directory, 'node_modules');
|
|
129
|
+
|
|
130
|
+
await fs.promises.mkdir(modules);
|
|
131
|
+
await fs.promises.symlink(runtime, path.join(modules, 'react-native'),
|
|
129
132
|
process.platform === 'win32' ? 'junction' : 'dir');
|
|
133
|
+
|
|
130
134
|
const output = await buildHermesBundleAsync({
|
|
131
135
|
code,
|
|
132
|
-
filename:
|
|
136
|
+
filename: entry,
|
|
133
137
|
map,
|
|
134
138
|
minify: true,
|
|
135
|
-
projectRoot:
|
|
139
|
+
projectRoot: directory,
|
|
136
140
|
});
|
|
137
141
|
|
|
138
142
|
if (!(output?.hbc instanceof Uint8Array) || typeof output.sourcemap !== 'string') {
|
|
@@ -140,11 +144,18 @@ async function exportEmbedUnlockedAsync(
|
|
|
140
144
|
}
|
|
141
145
|
|
|
142
146
|
await Promise.all([
|
|
143
|
-
fs.promises.writeFile(
|
|
144
|
-
fs.promises.writeFile(
|
|
147
|
+
fs.promises.writeFile(temp.bundle, output.hbc),
|
|
148
|
+
fs.promises.writeFile(temp.sourceMap, output.sourcemap),
|
|
145
149
|
]);
|
|
146
150
|
} catch (cause) {
|
|
147
|
-
if (cause instanceof HarmonyCliError)
|
|
151
|
+
if (cause instanceof HarmonyCliError) {
|
|
152
|
+
throw new HarmonyCliError(cause.code, cause.message, {
|
|
153
|
+
cause,
|
|
154
|
+
exitCode: cause.exitCode,
|
|
155
|
+
operation: cause.operation,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
148
159
|
throw new HarmonyCliError(
|
|
149
160
|
'ERR_HARMONY_EXPORT_HERMES',
|
|
150
161
|
'Expo failed to compile the Harmony bundle to Hermes bytecode.',
|
|
@@ -152,26 +163,24 @@ async function exportEmbedUnlockedAsync(
|
|
|
152
163
|
);
|
|
153
164
|
}
|
|
154
165
|
|
|
155
|
-
const bytecode = await assertHermesBundle(
|
|
156
|
-
await assertSourceMap(
|
|
166
|
+
const bytecode = await assertHermesBundle(temp.bundle);
|
|
167
|
+
await assertSourceMap(temp.sourceMap);
|
|
157
168
|
|
|
158
|
-
return await publishExportAsync(
|
|
169
|
+
return await publishExportAsync(root, paths, temp, entry, bytecode);
|
|
159
170
|
} finally {
|
|
160
|
-
await fs.promises.rm(
|
|
171
|
+
await fs.promises.rm(directory, { force: true, recursive: true });
|
|
161
172
|
}
|
|
162
173
|
}
|
|
163
174
|
|
|
164
175
|
async function exportEmbedAsync(
|
|
165
|
-
|
|
176
|
+
root: string,
|
|
166
177
|
options: ExportOptions = {}
|
|
167
178
|
): Promise<HarmonyExportManifest> {
|
|
168
179
|
return withHarmonyProjectLockAsync(
|
|
169
|
-
|
|
180
|
+
root,
|
|
170
181
|
'export:embed',
|
|
171
|
-
() => exportEmbedUnlockedAsync(
|
|
182
|
+
() => exportEmbedUnlockedAsync(root, options)
|
|
172
183
|
);
|
|
173
184
|
}
|
|
174
185
|
|
|
175
|
-
export {
|
|
176
|
-
exportEmbedAsync,
|
|
177
|
-
};
|
|
186
|
+
export { exportEmbedAsync };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { parseCommandArgs, type Command } from '../command';
|
|
2
|
+
import { parseExportEmbedArgs } from './options';
|
|
3
|
+
|
|
4
|
+
export const command: Command = async (argv, io) => {
|
|
5
|
+
const args = parseCommandArgs(parseExportEmbedArgs, argv, io);
|
|
6
|
+
if (!args) return 0;
|
|
7
|
+
|
|
8
|
+
const { projectRoot: root, options } = args;
|
|
9
|
+
const { exportEmbedAsync } = await import('./export.js');
|
|
10
|
+
|
|
11
|
+
const result = await exportEmbedAsync(root, options);
|
|
12
|
+
|
|
13
|
+
if (options.check) io.log('Harmony export bundle, assets, and source map are valid.');
|
|
14
|
+
else io.log(`Exported Hermes bytecode ${result.bundle.path} with ${result.assets.length} asset file(s).`);
|
|
15
|
+
|
|
16
|
+
return 0;
|
|
17
|
+
};
|
package/src/file.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@ import { isInside } from './path';
|
|
|
6
6
|
|
|
7
7
|
async function sha256File(file: string): Promise<string> {
|
|
8
8
|
return await new Promise<string>((resolve, reject) => {
|
|
9
|
-
const hash =
|
|
9
|
+
const hash = crypto.createHash('sha256');
|
|
10
10
|
const input = fs.createReadStream(file);
|
|
11
11
|
|
|
12
12
|
input.on('error', reject);
|
|
@@ -70,26 +70,26 @@ async function ensureSafeParent(root: string, destination: string): Promise<void
|
|
|
70
70
|
async function atomicCopy(source: string, destination: string, root: string): Promise<void> {
|
|
71
71
|
await ensureSafeParent(root, destination);
|
|
72
72
|
|
|
73
|
-
const
|
|
73
|
+
const temp = path.join(path.dirname(destination), `.${path.basename(destination)}.expo-${crypto.randomUUID()}.tmp`);
|
|
74
74
|
|
|
75
75
|
try {
|
|
76
|
-
await fs.promises.copyFile(source,
|
|
77
|
-
await fs.promises.rename(
|
|
76
|
+
await fs.promises.copyFile(source, temp, fs.constants.COPYFILE_EXCL);
|
|
77
|
+
await fs.promises.rename(temp, destination);
|
|
78
78
|
} finally {
|
|
79
|
-
await fs.promises.rm(
|
|
79
|
+
await fs.promises.rm(temp, { force: true });
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
async function atomicWriteJson(value: unknown, destination: string, root: string): Promise<void> {
|
|
84
84
|
await ensureSafeParent(root, destination);
|
|
85
85
|
|
|
86
|
-
const
|
|
86
|
+
const temp = path.join(path.dirname(destination), `.${path.basename(destination)}.expo-${crypto.randomUUID()}.tmp`);
|
|
87
87
|
|
|
88
88
|
try {
|
|
89
|
-
await fs.promises.writeFile(
|
|
90
|
-
await fs.promises.rename(
|
|
89
|
+
await fs.promises.writeFile(temp, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx' });
|
|
90
|
+
await fs.promises.rename(temp, destination);
|
|
91
91
|
} finally {
|
|
92
|
-
await fs.promises.rm(
|
|
92
|
+
await fs.promises.rm(temp, { force: true });
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -107,4 +107,13 @@ async function removeEmptyParents(file: string, stop: string): Promise<void> {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
function isNonEmptyRegularFile(file: string): boolean {
|
|
111
|
+
try {
|
|
112
|
+
const stat = fs.lstatSync(file);
|
|
113
|
+
return !stat.isSymbolicLink() && stat.isFile() && stat.size > 0;
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { atomicCopy, atomicWriteJson, describeFile, isNonEmptyRegularFile, listFiles, removeEmptyParents };
|
package/src/help.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const Help = `Usage: expo-harmony <command> [project] [options]
|
|
2
|
+
|
|
3
|
+
Commands:
|
|
4
|
+
start Start Expo Metro for Harmony development
|
|
5
|
+
prebuild Generate the Harmony native project with Expo CNG
|
|
6
|
+
prebuild --clean Safely recreate the managed Harmony directory
|
|
7
|
+
prebuild --check Compare generated desired state without project writes
|
|
8
|
+
build Build a HAP without selecting or contacting a device
|
|
9
|
+
doctor Validate config, versions, Metro, RNOH, SDK, and signing
|
|
10
|
+
modules list List native module candidates and their discovery source
|
|
11
|
+
modules inspect Resolve Harmony metadata (--package narrows the result)
|
|
12
|
+
modules verify Validate module config, registration conflicts and safe paths
|
|
13
|
+
export:embed Export validated Hermes bytecode, assets, and a source map
|
|
14
|
+
run Build, install, and launch the Harmony app
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--no-install Skip dependency install (prebuild) or HAP install (run)
|
|
18
|
+
--npm|--yarn|--pnpm|--bun
|
|
19
|
+
Select the package manager used by prebuild dependency install
|
|
20
|
+
--skip-dependency-update <packages>
|
|
21
|
+
Preserve comma-separated dependency versions
|
|
22
|
+
--device <id-or-name> Select an HDC target or start a local emulator by name
|
|
23
|
+
--variant <mode> Build debug or release (default: debug)
|
|
24
|
+
--no-bundler Use an already-running Expo Metro server
|
|
25
|
+
--app-id <bundleName>
|
|
26
|
+
Launch another installed app (requires --no-install when different)
|
|
27
|
+
--port <number> Metro and device reverse port (default: 8081)
|
|
28
|
+
--sync Re-run prebuild before building
|
|
29
|
+
--check Validate an existing export without writing
|
|
30
|
+
--reset-cache Reset Metro while exporting or starting
|
|
31
|
+
-c, --clear Alias for --reset-cache (start)
|
|
32
|
+
-h, --help Show this help
|
|
33
|
+
`;
|
package/src/index.ts
CHANGED
|
@@ -2,11 +2,13 @@ export { runAsync } from './cli';
|
|
|
2
2
|
export { buildHarmonyAsync } from './buildHap/build';
|
|
3
3
|
export type { HarmonyBuildOptions, HarmonyBuildResult } from './buildHap/build';
|
|
4
4
|
export { exportEmbedAsync } from './exportEmbed/export';
|
|
5
|
-
export { resolveHarmonyBuildPlanAsync
|
|
6
|
-
export {
|
|
5
|
+
export { resolveHarmonyBuildPlanAsync } from './native/project';
|
|
6
|
+
export type { HarmonyBuildPlan, HarmonyBuildMode } from './native/types';
|
|
7
|
+
export { resolveHarmonyToolchain } from './native/toolchain';
|
|
8
|
+
export { installHarmonyDependenciesAsync } from './native/install';
|
|
7
9
|
export { formatModulesResult, runModulesCommandAsync } from './modules/modules';
|
|
8
10
|
export type { ModulesCommandResult } from './modules/modules';
|
|
9
|
-
export { commitHarmonyNativeBuildCacheAsync, prepareHarmonyNativeBuildCacheAsync } from './
|
|
11
|
+
export { commitHarmonyNativeBuildCacheAsync, prepareHarmonyNativeBuildCacheAsync } from './native/cache';
|
|
10
12
|
export { runHarmonyAsync } from './run/run';
|
|
11
13
|
export type { HarmonyExportManifest } from './exportEmbed/manifest';
|
|
12
14
|
export type { HarmonyRunOptions, HarmonyRunResult } from './run/run';
|
package/src/log.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { parseCommandArgs, type Command } from '../command';
|
|
2
|
+
import { parseModulesArgs } from './options';
|
|
3
|
+
|
|
4
|
+
export const command: Command = async (argv, io) => {
|
|
5
|
+
const args = parseCommandArgs(parseModulesArgs, argv, io);
|
|
6
|
+
if (!args) return 0;
|
|
7
|
+
|
|
8
|
+
const { projectRoot: root, options } = args;
|
|
9
|
+
const { formatModulesResult, runModulesCommandAsync } = await import('./modules.js');
|
|
10
|
+
|
|
11
|
+
const result = await runModulesCommandAsync(root, options);
|
|
12
|
+
|
|
13
|
+
io.log(formatModulesResult(result));
|
|
14
|
+
|
|
15
|
+
return result.ok ? 0 : 1;
|
|
16
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import JSON5 from 'json5';
|
|
4
|
+
|
|
5
|
+
import { HarmonyCliError } from '../errors';
|
|
6
|
+
import type { BareHarmonyBuildPlan } from './types';
|
|
7
|
+
|
|
8
|
+
function isBareHarmonyProject(root: string): boolean {
|
|
9
|
+
return fs.existsSync(path.join(root, 'harmony'))
|
|
10
|
+
&& !fs.existsSync(path.join(root, 'harmony/.expo-harmony-template'))
|
|
11
|
+
&& !fs.existsSync(path.join(root, '.expo/harmony/cng-manifest.json'));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function record(value: unknown, field: string): Record<string, unknown> {
|
|
15
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
16
|
+
throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', `Native ${field} must be an object.`, { operation: 'resolve-build' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return value as Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function records(value: unknown, field: string): Record<string, unknown>[] {
|
|
23
|
+
if (!Array.isArray(value)) {
|
|
24
|
+
throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', `Native ${field} must be an array.`, { operation: 'resolve-build' });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return value.map(item => record(item, field));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readJson(file: string): Record<string, unknown> {
|
|
31
|
+
try {
|
|
32
|
+
return record(JSON5.parse(fs.readFileSync(file, 'utf8')), file);
|
|
33
|
+
} catch (cause) {
|
|
34
|
+
throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', `Cannot read native project file ${file}: ${cause.message}`, { cause, operation: 'resolve-build' });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function identifier(value: unknown, field: string): string {
|
|
39
|
+
if (typeof value !== 'string' || !/^[A-Za-z_][A-Za-z0-9_.]*$/.test(value)) {
|
|
40
|
+
throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', `The native project must specify a valid ${field}.`, { operation: 'resolve-build' });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function selectDefault(items: Record<string, unknown>[], field: string): Record<string, unknown> {
|
|
47
|
+
if (items.length === 0) {
|
|
48
|
+
throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', `No ${field} is defined in harmony/build-profile.json5.`, { operation: 'resolve-build' });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const defaults = items.filter(item => item.name === 'default');
|
|
52
|
+
|
|
53
|
+
if (defaults.length === 1) return defaults[0];
|
|
54
|
+
if (items.length === 1) return items[0];
|
|
55
|
+
|
|
56
|
+
throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', `Multiple ${field} are defined; name the intended default "default" in harmony/build-profile.json5.`, { operation: 'resolve-build' });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveBareHarmonyBuildPlan(root: string, mode: 'debug' | 'release'): BareHarmonyBuildPlan {
|
|
60
|
+
const harmony = path.join(root, 'harmony');
|
|
61
|
+
const entry = path.join(harmony, 'entry');
|
|
62
|
+
|
|
63
|
+
for (const directory of [harmony, entry]) {
|
|
64
|
+
if (!fs.existsSync(directory)
|
|
65
|
+
|| !fs.lstatSync(directory).isDirectory() || fs.lstatSync(directory).isSymbolicLink()) {
|
|
66
|
+
throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', `Bare Harmony projects require a regular directory at ${directory}.`, { operation: 'resolve-build' });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const profile = readJson(path.join(harmony, 'build-profile.json5'));
|
|
71
|
+
const app = record(readJson(path.join(harmony, 'AppScope/app.json5')).app, 'app');
|
|
72
|
+
const module = record(readJson(path.join(entry, 'src/main/module.json5')).module, 'module');
|
|
73
|
+
|
|
74
|
+
const name = identifier(module.name, 'module.name');
|
|
75
|
+
const registration = records(profile.modules, 'modules').find(item => item.name === name);
|
|
76
|
+
if (!registration || typeof registration.srcPath !== 'string'
|
|
77
|
+
|| path.resolve(harmony, registration.srcPath) !== entry || module.type !== 'entry') {
|
|
78
|
+
throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', 'Bare Harmony autolinking requires an entry module at harmony/entry, registered in build-profile.json5.', { operation: 'resolve-build' });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const config = record(profile.app, 'build-profile.app');
|
|
82
|
+
const selected = selectDefault(records(config.products, 'products'), 'products');
|
|
83
|
+
const product = identifier(selected.name, 'product name');
|
|
84
|
+
const targets = records(registration.targets, 'module targets').filter((item) => {
|
|
85
|
+
if (item.applyToProducts === undefined) return true;
|
|
86
|
+
if (!Array.isArray(item.applyToProducts) || item.applyToProducts.some(value => typeof value !== 'string')) {
|
|
87
|
+
throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', 'Native target.applyToProducts must be an array of product names.', { operation: 'resolve-build' });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return item.applyToProducts.includes(product);
|
|
91
|
+
});
|
|
92
|
+
const target = identifier(selectDefault(targets, 'module targets').name, 'target name');
|
|
93
|
+
|
|
94
|
+
const ability = identifier(module.mainElement, 'module.mainElement');
|
|
95
|
+
if (!records(module.abilities, 'abilities').some(item => item.name === ability)) {
|
|
96
|
+
throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', `Ability ${ability} is missing from module.json5.`, { operation: 'resolve-build' });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const bundle = identifier(app.bundleName, 'app.bundleName');
|
|
100
|
+
const signed = Boolean(selected.signingConfig);
|
|
101
|
+
if (signed && !records(config.signingConfigs, 'signingConfigs').some(item => item.name === selected.signingConfig)) {
|
|
102
|
+
throw new HarmonyCliError('ERR_HARMONY_NATIVE_PROJECT', `Signing config ${selected.signingConfig} is referenced by the product but is not defined.`, { operation: 'resolve-build' });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const rawfile = path.join(entry, 'src/main/resources/rawfile');
|
|
106
|
+
const metadata = path.join(root, '.expo/harmony/export');
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
workflow: 'bare',
|
|
110
|
+
abilityName: ability,
|
|
111
|
+
buildMode: mode,
|
|
112
|
+
bundleName: bundle,
|
|
113
|
+
harmonyRoot: harmony,
|
|
114
|
+
moduleName: name,
|
|
115
|
+
moduleRoot: entry,
|
|
116
|
+
productName: product,
|
|
117
|
+
targetName: target,
|
|
118
|
+
expectedHap: path.join(entry, `build/${target}/outputs/${target}/${name}-${target}-${signed ? 'signed' : 'unsigned'}.hap`),
|
|
119
|
+
hvigorArgs: ['--mode', 'module', '-p', `module=${name}@${target}`, '-p', `product=${product}`, '-p', `buildMode=${mode}`, '--no-daemon', 'assembleHap'],
|
|
120
|
+
exportPaths: {
|
|
121
|
+
bundle: path.join(rawfile, 'hermes_bundle.hbc'),
|
|
122
|
+
manifest: path.join(root, '.expo/harmony/export-manifest.json'),
|
|
123
|
+
metadataRoot: metadata,
|
|
124
|
+
rawfileRoot: rawfile,
|
|
125
|
+
sourceMap: path.join(metadata, 'hermes_bundle.hbc.map'),
|
|
126
|
+
},
|
|
127
|
+
nativeCache: {
|
|
128
|
+
invalidationRoots: [path.join(entry, '.cxx'), path.join(entry, 'build')],
|
|
129
|
+
stateFile: path.join(entry, '.cxx/.expo-harmony-native-dependencies.json'),
|
|
130
|
+
},
|
|
131
|
+
nativeInputs: {
|
|
132
|
+
lockfile: path.join(harmony, 'oh-package-lock.json5'),
|
|
133
|
+
manifest: path.join(harmony, 'oh-package.json5'),
|
|
134
|
+
},
|
|
135
|
+
projectFiles: {
|
|
136
|
+
hvigorConfig: path.join(harmony, 'hvigor/hvigor-config.json5'),
|
|
137
|
+
moduleHvigor: path.join(entry, 'hvigorfile.ts'),
|
|
138
|
+
moduleJson: path.join(entry, 'src/main/module.json5'),
|
|
139
|
+
projectBuildProfile: path.join(harmony, 'build-profile.json5'),
|
|
140
|
+
rootHvigor: path.join(harmony, 'hvigorfile.ts'),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { isBareHarmonyProject, resolveBareHarmonyBuildPlan };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { commitHarmonyNativeBuildCacheAsync, prepareHarmonyNativeBuildCacheAsync } from './cache';
|
|
4
|
+
import { installHarmonyDependenciesAsync } from './install';
|
|
5
|
+
import type { HarmonyToolchain } from './toolchain';
|
|
6
|
+
import type { HarmonyBuildPlan } from './types';
|
|
7
|
+
import { HarmonyCliError } from '../errors';
|
|
8
|
+
import { exportEmbedAsync } from '../exportEmbed/export';
|
|
9
|
+
import { isNonEmptyRegularFile } from '../file';
|
|
10
|
+
import { progress, type Log } from '../log';
|
|
11
|
+
import { toPosixPath } from '../path';
|
|
12
|
+
import { runCheckedAsync } from '../process';
|
|
13
|
+
import { timedAsync } from '../profile';
|
|
14
|
+
|
|
15
|
+
export interface NativeBuildResult {
|
|
16
|
+
export: null | { assetCount: number; bundleSha256: string; sourceMapSha256: string };
|
|
17
|
+
hapPath: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function buildNativeAsync(
|
|
21
|
+
root: string,
|
|
22
|
+
plan: HarmonyBuildPlan,
|
|
23
|
+
tools: HarmonyToolchain,
|
|
24
|
+
options: { io?: Log; resetCache?: boolean },
|
|
25
|
+
steps: Record<string, number>
|
|
26
|
+
): Promise<NativeBuildResult> {
|
|
27
|
+
let exported: NativeBuildResult['export'] = null;
|
|
28
|
+
if (plan.buildMode === 'release') {
|
|
29
|
+
progress(options, 'Exporting the release Hermes bundle');
|
|
30
|
+
const manifest = await timedAsync(steps, 'export', () => exportEmbedAsync(root, {
|
|
31
|
+
resetCache: options.resetCache,
|
|
32
|
+
skipDoctor: true,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
exported = {
|
|
36
|
+
assetCount: manifest.assets.length,
|
|
37
|
+
bundleSha256: manifest.bundle.sha256,
|
|
38
|
+
sourceMapSha256: manifest.sourceMap.sha256,
|
|
39
|
+
};
|
|
40
|
+
} else {
|
|
41
|
+
steps.export = 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
progress(options, 'Installing Harmony project dependencies');
|
|
45
|
+
await timedAsync(steps, 'ohpm', () => installHarmonyDependenciesAsync(plan, tools));
|
|
46
|
+
|
|
47
|
+
progress(options, 'Checking Harmony native dependency cache');
|
|
48
|
+
const cache = await timedAsync(steps, 'nativeCache', () => prepareHarmonyNativeBuildCacheAsync(root, plan));
|
|
49
|
+
if (cache.changed) progress(options, 'Invalidated stale Harmony native build objects');
|
|
50
|
+
|
|
51
|
+
progress(options, `Building the ${plan.buildMode} HAP`);
|
|
52
|
+
await timedAsync(steps, 'build', () => runCheckedAsync(tools.hvigor.command, [
|
|
53
|
+
...tools.hvigor.args,
|
|
54
|
+
...plan.hvigorArgs,
|
|
55
|
+
], {
|
|
56
|
+
code: 'ERR_HARMONY_BUILD_FAILED',
|
|
57
|
+
cwd: plan.harmonyRoot,
|
|
58
|
+
env: {
|
|
59
|
+
...process.env,
|
|
60
|
+
EXPO_HARMONY_NODE: process.env.EXPO_HARMONY_NODE || process.execPath,
|
|
61
|
+
EXPO_METRO_TARGET: 'harmony',
|
|
62
|
+
HERMES_V1_ENABLED: 'true',
|
|
63
|
+
...(plan.workflow === 'bare' ? { EXPO_HARMONY_NATIVE_PREPARED: '1' } : {}),
|
|
64
|
+
...(plan.buildMode === 'release' ? { EXPO_HARMONY_BUNDLE_PREBUILT: '1' } : {}),
|
|
65
|
+
...(tools.sdkHome && !process.env.DEVECO_SDK_HOME
|
|
66
|
+
? { DEVECO_SDK_HOME: tools.sdkHome }
|
|
67
|
+
: {}),
|
|
68
|
+
},
|
|
69
|
+
message: 'Hvigor build',
|
|
70
|
+
operation: 'hvigor-build',
|
|
71
|
+
timeoutMs: 15 * 60_000,
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
if (!isNonEmptyRegularFile(plan.expectedHap)) {
|
|
75
|
+
throw new HarmonyCliError('ERR_HARMONY_HAP_MISSING', 'Hvigor completed without producing the expected non-empty regular HAP.', { operation: 'verify-hap' });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await timedAsync(steps, 'nativeCacheCommit', () => commitHarmonyNativeBuildCacheAsync(cache));
|
|
79
|
+
|
|
80
|
+
return { export: exported, hapPath: toPosixPath(path.relative(root, plan.expectedHap)) };
|
|
81
|
+
}
|