@expo-harmony/cli 55.0.26-harmony.1 → 55.0.26-harmony.10
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 +87 -38
- package/build/args.d.ts +19 -0
- package/build/args.js +27 -0
- package/build/bin/expo-harmony.d.ts +2 -0
- package/build/bin/expo-harmony.js +1 -1
- package/build/buildHap/build.d.ts +18 -0
- package/build/buildHap/build.js +38 -0
- package/build/buildHap/index.d.ts +2 -0
- package/build/buildHap/index.js +19 -0
- package/build/buildHap/options.d.ts +7 -0
- package/build/buildHap/options.js +30 -0
- package/build/cli.d.ts +3 -0
- package/build/cli.js +40 -0
- package/build/command.d.ts +9 -0
- package/build/command.js +20 -0
- package/build/development/manifest.d.ts +1 -0
- package/build/development/manifest.js +100 -0
- package/build/development/protocol.d.ts +2 -0
- package/build/development/protocol.js +26 -0
- package/build/development/session.d.ts +5 -0
- package/build/development/session.js +66 -0
- package/build/doctor/doctor.d.ts +20 -0
- package/build/doctor/doctor.js +226 -0
- package/build/doctor/index.d.ts +2 -0
- package/build/doctor/index.js +16 -0
- package/build/doctor/options.d.ts +5 -0
- package/build/doctor/options.js +15 -0
- package/build/entry.d.ts +2 -0
- package/build/entry.js +47 -0
- package/build/errors.d.ts +12 -0
- package/build/errors.js +16 -0
- package/build/expo.d.ts +19 -0
- package/build/expo.js +53 -0
- package/build/exportEmbed/export.d.ts +18 -0
- package/build/exportEmbed/export.js +124 -0
- package/build/exportEmbed/index.d.ts +2 -0
- package/build/exportEmbed/index.js +19 -0
- package/build/exportEmbed/manifest.d.ts +44 -0
- package/build/exportEmbed/manifest.js +171 -0
- package/build/exportEmbed/options.d.ts +7 -0
- package/build/exportEmbed/options.js +22 -0
- package/build/file.d.ts +10 -0
- package/build/file.js +110 -0
- package/build/help.d.ts +1 -0
- package/build/help.js +44 -0
- package/build/index.d.ts +14 -0
- package/build/index.js +6 -5
- package/build/internal/development.d.ts +1 -0
- package/build/internal/development.js +8 -0
- package/build/internal/export.d.ts +1 -0
- package/build/internal/export.js +80 -0
- 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/modules/modules.d.ts +42 -0
- package/build/modules/modules.js +84 -0
- package/build/modules/options.d.ts +10 -0
- package/build/modules/options.js +45 -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 +69 -0
- package/build/native/cache.d.ts +11 -0
- package/build/native/cache.js +73 -0
- package/build/native/hvigor.d.ts +3 -0
- package/build/native/hvigor.js +83 -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/native/toolchain.js +172 -0
- package/build/native/types.d.ts +47 -0
- package/build/native/types.js +2 -0
- package/build/path.d.ts +4 -0
- package/build/path.js +27 -0
- package/build/prebuild/check.d.ts +10 -0
- package/build/prebuild/check.js +151 -0
- package/build/prebuild/clean.d.ts +2 -0
- package/build/prebuild/clean.js +50 -0
- package/build/prebuild/index.d.ts +2 -0
- package/build/prebuild/index.js +51 -0
- package/build/prebuild/options.d.ts +10 -0
- package/build/prebuild/options.js +50 -0
- package/build/prebuild/prebuild.d.ts +6 -0
- package/build/prebuild/prebuild.js +70 -0
- package/build/prebuild/template.d.ts +8 -0
- package/build/prebuild/template.js +86 -0
- package/build/process.d.ts +41 -0
- package/build/process.js +324 -0
- package/build/profile.d.ts +1 -0
- package/build/profile.js +12 -0
- package/build/project.d.ts +2 -0
- package/build/project.js +23 -0
- package/build/projectLock.d.ts +2 -0
- package/build/projectLock.js +185 -0
- package/build/run/devices.d.ts +29 -0
- package/build/run/devices.js +216 -0
- package/build/run/emulators.d.ts +13 -0
- package/build/run/emulators.js +82 -0
- package/build/run/index.d.ts +2 -0
- package/build/run/index.js +25 -0
- package/build/run/metro.d.ts +21 -0
- package/build/run/metro.js +162 -0
- package/build/run/options.d.ts +14 -0
- package/build/run/options.js +62 -0
- package/build/run/run.d.ts +46 -0
- package/build/run/run.js +135 -0
- package/build/runtime/config.d.ts +35 -0
- package/build/runtime/config.js +118 -0
- package/build/runtime/contract.d.ts +5 -0
- package/build/runtime/contract.js +108 -0
- package/build/runtime/fingerprint.d.ts +1 -0
- package/build/runtime/fingerprint.js +44 -0
- package/build/runtime/index.d.ts +2 -0
- package/build/runtime/index.js +50 -0
- package/build/start/index.d.ts +2 -0
- package/build/start/index.js +38 -0
- package/build/start/options.d.ts +9 -0
- package/build/start/options.js +38 -0
- package/build/updates/export.d.ts +14 -0
- package/build/updates/export.js +46 -0
- package/build/updates/index.d.ts +2 -0
- package/build/updates/index.js +96 -0
- package/build/upstream.d.ts +9 -0
- package/build/upstream.js +17 -0
- package/package.json +21 -5
- 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 +27 -225
- package/src/command.ts +24 -0
- package/src/development/manifest.ts +106 -0
- package/src/development/protocol.ts +37 -0
- package/src/development/session.ts +83 -0
- package/src/doctor/doctor.ts +67 -62
- package/src/doctor/index.ts +16 -0
- package/src/exportEmbed/export.ts +73 -72
- package/src/exportEmbed/index.ts +17 -0
- package/src/file.ts +20 -11
- package/src/help.ts +41 -0
- package/src/index.ts +5 -3
- package/src/internal/development.ts +8 -0
- package/src/internal/export.ts +82 -0
- 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 +83 -0
- package/src/{run → native}/cache.ts +10 -9
- package/src/native/hvigor.ts +88 -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} +38 -140
- package/src/native/types.ts +44 -0
- package/src/prebuild/check.ts +56 -31
- 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 +175 -64
- 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 +10 -1
- package/src/run/options.ts +2 -0
- package/src/run/run.ts +63 -129
- package/src/runtime/config.ts +150 -0
- package/src/runtime/contract.ts +165 -0
- package/src/runtime/fingerprint.ts +39 -0
- package/src/runtime/index.ts +54 -0
- package/src/start/index.ts +37 -0
- package/src/start/options.ts +12 -0
- package/src/updates/export.ts +60 -0
- package/src/updates/index.ts +89 -0
- package/src/buildHap/common.ts +0 -145
- package/src/run/install.ts +0 -41
package/src/buildHap/build.ts
CHANGED
|
@@ -1,39 +1,16 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
|
|
3
|
-
import { HarmonyCliError } from '../errors';
|
|
4
|
-
import { exportEmbedAsync } from '../exportEmbed/export';
|
|
5
|
-
import type { HarmonyExportManifest } from '../exportEmbed/manifest';
|
|
6
|
-
import { toPosixPath } from '../path';
|
|
7
1
|
import { withHarmonyProjectLockAsync } from '../projectLock';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from '../run/cache';
|
|
12
|
-
import { installHarmonyDependenciesAsync } from '../run/install';
|
|
13
|
-
import {
|
|
14
|
-
resolveHarmonyBuildPlanAsync,
|
|
15
|
-
resolveHarmonyToolchain,
|
|
16
|
-
} from '../tools';
|
|
17
|
-
import {
|
|
18
|
-
ensureGeneratedProjectAsync,
|
|
19
|
-
isNonEmptyRegularFile,
|
|
20
|
-
progress,
|
|
21
|
-
runCheckedAsync,
|
|
22
|
-
timed,
|
|
23
|
-
} from './common';
|
|
2
|
+
import { buildNativeAsync, type NativeBuildResult } from '../native/build';
|
|
3
|
+
import { prepareNativeProjectAsync } from '../native/prepareProject';
|
|
4
|
+
import { resolveHarmonyToolchain } from '../native/toolchain';
|
|
24
5
|
|
|
25
6
|
export interface HarmonyBuildOptions {
|
|
26
7
|
io?: Pick<Console, 'error' | 'log' | 'warn'>;
|
|
27
|
-
/** @internal For release verification after a successful isolated prebuild check. */
|
|
28
|
-
skipGeneratedProjectCheck?: boolean;
|
|
29
8
|
sync?: boolean;
|
|
30
9
|
variant?: 'debug' | 'release';
|
|
31
10
|
}
|
|
32
11
|
|
|
33
|
-
export interface HarmonyBuildResult {
|
|
12
|
+
export interface HarmonyBuildResult extends NativeBuildResult {
|
|
34
13
|
bundleName: string;
|
|
35
|
-
export: null | { assetCount: number; bundleSha256: string; sourceMapSha256: string };
|
|
36
|
-
hapPath: string;
|
|
37
14
|
headless: true;
|
|
38
15
|
installed: false;
|
|
39
16
|
launched: false;
|
|
@@ -48,13 +25,12 @@ type NormalizedBuildOptions = Required<HarmonyBuildOptions> & {
|
|
|
48
25
|
};
|
|
49
26
|
|
|
50
27
|
async function buildHarmonyUnlockedAsync(
|
|
51
|
-
|
|
28
|
+
root: string,
|
|
52
29
|
options: HarmonyBuildOptions = {}
|
|
53
30
|
): Promise<HarmonyBuildResult> {
|
|
54
|
-
const
|
|
31
|
+
const settings: NormalizedBuildOptions = {
|
|
55
32
|
io: options.io || console,
|
|
56
33
|
requireDeviceTools: false,
|
|
57
|
-
skipGeneratedProjectCheck: Boolean(options.skipGeneratedProjectCheck),
|
|
58
34
|
sync: Boolean(options.sync),
|
|
59
35
|
variant: options.variant || 'debug',
|
|
60
36
|
};
|
|
@@ -66,98 +42,30 @@ async function buildHarmonyUnlockedAsync(
|
|
|
66
42
|
metroPort: 0,
|
|
67
43
|
};
|
|
68
44
|
|
|
69
|
-
await
|
|
70
|
-
|
|
71
|
-
const plan = await timed(steps, 'buildPlan', () => resolveHarmonyBuildPlanAsync(
|
|
72
|
-
projectRoot,
|
|
73
|
-
{ buildMode: normalizedOptions.variant }
|
|
74
|
-
));
|
|
75
|
-
const toolchain = resolveHarmonyToolchain();
|
|
76
|
-
|
|
77
|
-
let exportManifest: HarmonyExportManifest | null = null;
|
|
78
|
-
if (normalizedOptions.variant === 'release') {
|
|
79
|
-
progress(normalizedOptions, 'Exporting the release Hermes bundle');
|
|
80
|
-
exportManifest = await timed(steps, 'export', () => exportEmbedAsync(
|
|
81
|
-
projectRoot,
|
|
82
|
-
{ skipDoctor: true }
|
|
83
|
-
));
|
|
84
|
-
} else {
|
|
85
|
-
steps.export = 0;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
progress(normalizedOptions, 'Installing Harmony project dependencies');
|
|
89
|
-
await timed(steps, 'ohpm', () => installHarmonyDependenciesAsync(plan, toolchain));
|
|
90
|
-
|
|
91
|
-
progress(normalizedOptions, 'Checking Harmony native dependency cache');
|
|
92
|
-
const nativeBuildCache = await timed(steps, 'nativeCache', () => (
|
|
93
|
-
prepareHarmonyNativeBuildCacheAsync(projectRoot, plan)
|
|
94
|
-
));
|
|
95
|
-
|
|
96
|
-
if (nativeBuildCache.changed) {
|
|
97
|
-
progress(normalizedOptions, 'Invalidated stale Harmony native build objects');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
progress(normalizedOptions, `Building the ${normalizedOptions.variant} HAP without a device`);
|
|
101
|
-
const buildEnv = {
|
|
102
|
-
...process.env,
|
|
103
|
-
EXPO_HARMONY_NODE: process.env.EXPO_HARMONY_NODE || process.execPath,
|
|
104
|
-
EXPO_METRO_TARGET: 'harmony',
|
|
105
|
-
HERMES_V1_ENABLED: 'true',
|
|
106
|
-
...(normalizedOptions.variant === 'release' ? { EXPO_HARMONY_BUNDLE_PREBUILT: '1' } : {}),
|
|
107
|
-
...(toolchain.sdkHome && !process.env.DEVECO_SDK_HOME
|
|
108
|
-
? { DEVECO_SDK_HOME: toolchain.sdkHome }
|
|
109
|
-
: {}),
|
|
110
|
-
};
|
|
111
|
-
await timed(steps, 'build', () => runCheckedAsync(toolchain.hvigor.command, [
|
|
112
|
-
...toolchain.hvigor.args,
|
|
113
|
-
...plan.hvigorArgs,
|
|
114
|
-
], {
|
|
115
|
-
code: 'ERR_HARMONY_BUILD_FAILED',
|
|
116
|
-
cwd: plan.harmonyRoot,
|
|
117
|
-
env: buildEnv,
|
|
118
|
-
message: 'Hvigor build',
|
|
119
|
-
operation: 'hvigor-build',
|
|
120
|
-
timeoutMs: 15 * 60_000,
|
|
121
|
-
}));
|
|
122
|
-
|
|
123
|
-
if (!isNonEmptyRegularFile(plan.expectedHap)) {
|
|
124
|
-
throw new HarmonyCliError(
|
|
125
|
-
'ERR_HARMONY_HAP_MISSING',
|
|
126
|
-
'Hvigor completed without producing the expected non-empty regular HAP.',
|
|
127
|
-
{ operation: 'verify-hap' }
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
await timed(steps, 'nativeCacheCommit', () => commitHarmonyNativeBuildCacheAsync(nativeBuildCache));
|
|
45
|
+
const plan = await prepareNativeProjectAsync(root, settings, steps);
|
|
46
|
+
const built = await buildNativeAsync(root, plan, resolveHarmonyToolchain(), settings, steps);
|
|
132
47
|
|
|
133
48
|
return {
|
|
134
49
|
bundleName: plan.bundleName,
|
|
135
|
-
|
|
136
|
-
? {
|
|
137
|
-
assetCount: exportManifest.assets.length,
|
|
138
|
-
bundleSha256: exportManifest.bundle.sha256,
|
|
139
|
-
sourceMapSha256: exportManifest.sourceMap.sha256,
|
|
140
|
-
}
|
|
141
|
-
: null,
|
|
142
|
-
hapPath: toPosixPath(path.relative(projectRoot, plan.expectedHap)),
|
|
50
|
+
...built,
|
|
143
51
|
headless: true,
|
|
144
52
|
installed: false,
|
|
145
53
|
launched: false,
|
|
146
54
|
ok: true,
|
|
147
55
|
schemaVersion: 1,
|
|
148
56
|
steps,
|
|
149
|
-
variant:
|
|
57
|
+
variant: settings.variant,
|
|
150
58
|
};
|
|
151
59
|
}
|
|
152
60
|
|
|
153
61
|
async function buildHarmonyAsync(
|
|
154
|
-
|
|
62
|
+
root: string,
|
|
155
63
|
options: HarmonyBuildOptions = {}
|
|
156
64
|
): Promise<HarmonyBuildResult> {
|
|
157
65
|
return withHarmonyProjectLockAsync(
|
|
158
|
-
|
|
66
|
+
root,
|
|
159
67
|
'build',
|
|
160
|
-
() => buildHarmonyUnlockedAsync(
|
|
68
|
+
() => buildHarmonyUnlockedAsync(root, options)
|
|
161
69
|
);
|
|
162
70
|
}
|
|
163
71
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { parseCommandArgs, type Command } from '../command';
|
|
2
|
+
import { parseBuildArgs } from './options';
|
|
3
|
+
|
|
4
|
+
export const command: Command = async (argv, io) => {
|
|
5
|
+
const args = parseCommandArgs(parseBuildArgs, argv, io);
|
|
6
|
+
if (!args) return 0;
|
|
7
|
+
|
|
8
|
+
const { projectRoot: root, options } = args;
|
|
9
|
+
const { buildHarmonyAsync } = await import('./build.js');
|
|
10
|
+
|
|
11
|
+
const result = await buildHarmonyAsync(root, {
|
|
12
|
+
...options,
|
|
13
|
+
io,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
io.log(`Built ${result.variant} HAP at ${result.hapPath}; no device actions were performed.`);
|
|
17
|
+
|
|
18
|
+
return 0;
|
|
19
|
+
};
|
package/src/cli.ts
CHANGED
|
@@ -1,242 +1,44 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import { parseBuildArgs } from './buildHap/options';
|
|
4
|
-
import { parseDoctorArgs } from './doctor/options';
|
|
1
|
+
import type { Command } from './command';
|
|
5
2
|
import { HarmonyCliError } from './errors';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
modules inspect Resolve Harmony metadata (--package narrows the result)
|
|
25
|
-
modules verify Validate module config, registration conflicts and safe paths
|
|
26
|
-
export:embed Export validated Hermes bytecode, assets, and a source map
|
|
27
|
-
run Build, install, and launch the Harmony app
|
|
28
|
-
|
|
29
|
-
Options:
|
|
30
|
-
--no-install Skip dependency install (prebuild) or HAP install (run)
|
|
31
|
-
--npm|--yarn|--pnpm|--bun
|
|
32
|
-
Select the package manager used by prebuild dependency install
|
|
33
|
-
--skip-dependency-update <packages>
|
|
34
|
-
Preserve comma-separated dependency versions
|
|
35
|
-
--device <id-or-name> Select an HDC target or start a local emulator by name
|
|
36
|
-
--variant <mode> Build debug or release (default: debug)
|
|
37
|
-
--no-bundler Use an already-running Expo Metro server
|
|
38
|
-
--app-id <bundleName>
|
|
39
|
-
Launch another installed app (requires --no-install when different)
|
|
40
|
-
--port <number> Metro and device reverse port (default: 8081)
|
|
41
|
-
--sync Re-run prebuild before building
|
|
42
|
-
--check Validate an existing export without writing
|
|
43
|
-
--reset-cache Reset Metro while exporting or starting
|
|
44
|
-
-c, --clear Alias for --reset-cache (start)
|
|
45
|
-
-h, --help Show this help
|
|
46
|
-
`;
|
|
3
|
+
import { Help } from './help';
|
|
4
|
+
import type { Log } from './log';
|
|
5
|
+
|
|
6
|
+
const commands: Record<string, () => Promise<{ command: Command }>> = {
|
|
7
|
+
'runtime': () => import('./runtime/index.js'),
|
|
8
|
+
'start': () => import('./start/index.js'),
|
|
9
|
+
'prebuild': () => import('./prebuild/index.js'),
|
|
10
|
+
'build': () => import('./buildHap/index.js'),
|
|
11
|
+
'doctor': () => import('./doctor/index.js'),
|
|
12
|
+
'modules': () => import('./modules/index.js'),
|
|
13
|
+
'export': () => import('./updates/index.js'),
|
|
14
|
+
'export:embed': () => import('./exportEmbed/index.js'),
|
|
15
|
+
'run': () => import('./run/index.js'),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export async function runAsync(argv: string[] = process.argv.slice(2), io: Log = console): Promise<number> {
|
|
19
|
+
process.env.EXPO_HARMONY = '1';
|
|
20
|
+
process.env.EXPO_METRO_TARGET = 'harmony';
|
|
47
21
|
|
|
48
|
-
type Invocation = { command: 'help' }
|
|
49
|
-
| { command: 'build'; parsed: ReturnType<typeof parseBuildArgs>; projectRoot: string }
|
|
50
|
-
| { command: 'doctor'; parsed: ReturnType<typeof parseDoctorArgs>; projectRoot: string }
|
|
51
|
-
| { command: 'export:embed'; parsed: ReturnType<typeof parseExportEmbedArgs>; projectRoot: string }
|
|
52
|
-
| { command: 'modules'; parsed: ReturnType<typeof parseModulesArgs>; projectRoot: string }
|
|
53
|
-
| { command: 'prebuild'; parsed: ReturnType<typeof parsePrebuildArgs>; projectRoot: string }
|
|
54
|
-
| { command: 'run'; parsed: ReturnType<typeof parseRunArgs>; projectRoot: string }
|
|
55
|
-
| { command: 'start'; parsed: ReturnType<typeof parseStartArgs>; projectRoot: string };
|
|
56
|
-
|
|
57
|
-
function parseInvocation(argv: string[]): Invocation {
|
|
58
22
|
if (argv.length === 0 || (argv.length === 1 && ['--help', '-h'].includes(argv[0]))) {
|
|
59
|
-
return { command: 'help' };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const command = argv[0] as Exclude<Invocation['command'], 'help'>;
|
|
63
|
-
if (!['build', 'prebuild', 'doctor', 'export:embed', 'modules', 'run', 'start'].includes(command)) {
|
|
64
|
-
throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', `Unknown command: ${command}`, {
|
|
65
|
-
operation: 'parse-arguments',
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const parsed = command === 'build'
|
|
70
|
-
? parseBuildArgs(argv.slice(1))
|
|
71
|
-
: command === 'prebuild'
|
|
72
|
-
? parsePrebuildArgs(argv.slice(1), { allowProject: true })
|
|
73
|
-
: command === 'doctor'
|
|
74
|
-
? parseDoctorArgs(argv.slice(1))
|
|
75
|
-
: command === 'export:embed'
|
|
76
|
-
? parseExportEmbedArgs(argv.slice(1))
|
|
77
|
-
: command === 'modules'
|
|
78
|
-
? parseModulesArgs(argv.slice(1))
|
|
79
|
-
: command === 'start'
|
|
80
|
-
? parseStartArgs(argv.slice(1))
|
|
81
|
-
: parseRunArgs(argv.slice(1));
|
|
82
|
-
if (parsed.help) return { command: 'help' };
|
|
83
|
-
|
|
84
|
-
const projectRoot = resolveProject(parsed.project ? path.resolve(parsed.project) : process.cwd());
|
|
85
|
-
|
|
86
|
-
return { command, parsed, projectRoot } as Invocation;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function runAsync(
|
|
90
|
-
argv: string[] = process.argv.slice(2),
|
|
91
|
-
io: Pick<Console, 'error' | 'log' | 'warn'> = console
|
|
92
|
-
): Promise<number> {
|
|
93
|
-
const invocation = parseInvocation(argv);
|
|
94
|
-
if (invocation.command === 'help') {
|
|
95
23
|
io.log(Help);
|
|
96
24
|
return 0;
|
|
97
25
|
}
|
|
98
26
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
...invocation.parsed,
|
|
103
|
-
interactive: true,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
if (metro.owner === 'existing') {
|
|
108
|
-
io.log(`Expo Metro is already running on port ${metro.port}.`);
|
|
109
|
-
if (invocation.parsed.resetCache) {
|
|
110
|
-
io.warn('Stop the existing Metro server and run this command again to reset its cache.');
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
io.log('\n› Logs for your project will appear below. Press Ctrl+C to exit.');
|
|
114
|
-
await metro.waitAsync();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return 0;
|
|
118
|
-
} finally {
|
|
119
|
-
await metro.stop();
|
|
120
|
-
}
|
|
27
|
+
const name = argv[0];
|
|
28
|
+
if (!Object.hasOwn(commands, name)) {
|
|
29
|
+
throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', `Unknown command: ${name}`, { operation: 'parse-arguments' });
|
|
121
30
|
}
|
|
122
31
|
|
|
123
|
-
|
|
124
|
-
const { doctorAsync, formatDoctor } = await import('./doctor/doctor.js');
|
|
125
|
-
const result = await doctorAsync(invocation.projectRoot, { requireBuildTools: true });
|
|
126
|
-
|
|
127
|
-
io.log(formatDoctor(result));
|
|
128
|
-
|
|
129
|
-
return result.ok ? 0 : 1;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (invocation.command === 'build') {
|
|
133
|
-
const { buildHarmonyAsync } = await import('./buildHap/build.js');
|
|
134
|
-
const result = await buildHarmonyAsync(invocation.projectRoot, {
|
|
135
|
-
...invocation.parsed,
|
|
136
|
-
io,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
io.log(`Built ${result.variant} HAP at ${result.hapPath}; no device actions were performed.`);
|
|
32
|
+
const { command } = await commands[name]();
|
|
140
33
|
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (invocation.command === 'export:embed') {
|
|
145
|
-
const { exportEmbedAsync } = await import('./exportEmbed/export.js');
|
|
146
|
-
const result = await exportEmbedAsync(invocation.projectRoot, invocation.parsed);
|
|
147
|
-
|
|
148
|
-
if (invocation.parsed.check) io.log('Harmony export bundle, assets, and source map are valid.');
|
|
149
|
-
else io.log(`Exported Hermes bytecode ${result.bundle.path} with ${result.assets.length} asset file(s).`);
|
|
150
|
-
|
|
151
|
-
return 0;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (invocation.command === 'modules') {
|
|
155
|
-
const { formatModulesResult, runModulesCommandAsync } = await import('./modules/modules.js');
|
|
156
|
-
const result = await runModulesCommandAsync(invocation.projectRoot, invocation.parsed);
|
|
157
|
-
|
|
158
|
-
io.log(formatModulesResult(result));
|
|
159
|
-
return result.ok ? 0 : 1;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (invocation.command === 'run') {
|
|
163
|
-
const { runHarmonySessionAsync } = await import('./run/run.js');
|
|
164
|
-
|
|
165
|
-
const session = await runHarmonySessionAsync(invocation.projectRoot, {
|
|
166
|
-
...invocation.parsed,
|
|
167
|
-
interactiveBundler: true,
|
|
168
|
-
io,
|
|
169
|
-
});
|
|
170
|
-
const { result } = session;
|
|
171
|
-
|
|
172
|
-
io.log(`Launched ${result.bundleName} on ${result.device.id} (${result.variant}).`);
|
|
173
|
-
if (session.metro.owner === 'started') {
|
|
174
|
-
io.log('\n› Logs for your project will appear below. Press Ctrl+C to exit.');
|
|
175
|
-
await session.metro.waitAsync();
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return 0;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const { check, clean, passthrough } = invocation.parsed;
|
|
182
|
-
|
|
183
|
-
if (check && passthrough.length) {
|
|
184
|
-
throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--check cannot be combined with mutating prebuild options.');
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return withHarmonyProjectLockAsync(
|
|
188
|
-
invocation.projectRoot,
|
|
189
|
-
check ? 'prebuild-check' : 'prebuild',
|
|
190
|
-
async () => {
|
|
191
|
-
if (clean) {
|
|
192
|
-
const { assertSafeCleanTarget } = await import('./prebuild/clean.js');
|
|
193
|
-
await assertSafeCleanTarget(invocation.projectRoot);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const { doctorAsync, formatDoctor } = await import('./doctor/doctor.js');
|
|
197
|
-
const doctor = await doctorAsync(invocation.projectRoot, {
|
|
198
|
-
requireBuildTools: false,
|
|
199
|
-
validateGeneratedProject: !clean,
|
|
200
|
-
validateModules: false,
|
|
201
|
-
});
|
|
202
|
-
if (!doctor.ok) {
|
|
203
|
-
io.error(formatDoctor(doctor));
|
|
204
|
-
throw new HarmonyCliError('ERR_HARMONY_DOCTOR_FAILED', 'Harmony doctor found blocking errors.', {
|
|
205
|
-
operation: 'doctor',
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
for (const item of doctor.checks.filter(item => item.status === 'warn')) io.warn(`! ${item.message}`);
|
|
210
|
-
|
|
211
|
-
if (check) {
|
|
212
|
-
const { checkAsync } = await import('./prebuild/check.js');
|
|
213
|
-
const result = await checkAsync(invocation.projectRoot);
|
|
214
|
-
|
|
215
|
-
if (result.clean) io.log('Harmony CNG output is up to date.');
|
|
216
|
-
else for (const change of result.changes) io.log(`${change.type}: ${change.path}`);
|
|
217
|
-
|
|
218
|
-
return result.clean ? 0 : 2;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const { prebuildParsedAsync } = await import('./prebuild/prebuild.js');
|
|
222
|
-
await prebuildParsedAsync(invocation.projectRoot, passthrough);
|
|
223
|
-
|
|
224
|
-
return 0;
|
|
225
|
-
});
|
|
34
|
+
return command(argv.slice(1), io);
|
|
226
35
|
}
|
|
227
36
|
|
|
228
|
-
async function
|
|
37
|
+
export async function mainAsync(argv: string[] = process.argv.slice(2)): Promise<void> {
|
|
229
38
|
try {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
process.exitCode = code;
|
|
39
|
+
process.exitCode = await runAsync(argv);
|
|
233
40
|
} catch (error) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
console.error(`[${code}] ${error.message}`);
|
|
237
|
-
|
|
41
|
+
console.error(`[${error.code || 'ERR_HARMONY_UNKNOWN'}] ${error.message}`);
|
|
238
42
|
process.exitCode = error.exitCode || 1;
|
|
239
43
|
}
|
|
240
44
|
}
|
|
241
|
-
|
|
242
|
-
export { main, runAsync };
|
package/src/command.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { Help } from './help';
|
|
4
|
+
import type { Log } from './log';
|
|
5
|
+
import { resolveProject } from './project';
|
|
6
|
+
|
|
7
|
+
export type Command = (argv: string[], io: Log) => Promise<number>;
|
|
8
|
+
|
|
9
|
+
export function parseCommandArgs<T extends { help: boolean; project?: string }>(
|
|
10
|
+
parse: (argv: string[]) => T,
|
|
11
|
+
argv: string[],
|
|
12
|
+
io: Log
|
|
13
|
+
): { options: T; projectRoot: string } | null {
|
|
14
|
+
const options = parse(argv);
|
|
15
|
+
if (options.help) {
|
|
16
|
+
io.log(Help);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
options,
|
|
22
|
+
projectRoot: resolveProject(options.project ? path.resolve(options.project) : process.cwd()),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
|
|
4
|
+
import { HarmonyCliError } from '../errors';
|
|
5
|
+
import { resolveExpoCli } from '../expo';
|
|
6
|
+
import { resolveHarmonyBuildPlanAsync } from '../native/project';
|
|
7
|
+
import { resolveRuntimeRequirementsAsync } from '../runtime/contract';
|
|
8
|
+
import { createHarmonyLaunchLink, HarmonyManifestPath } from './protocol';
|
|
9
|
+
|
|
10
|
+
async function createManifestResponseAsync(middleware, options, require: NodeRequire) {
|
|
11
|
+
const { ExpoGoManifestHandlerMiddleware: Manifest, ResponseContentType: Types } = require('./build/src/start/server/middleware/ExpoGoManifestHandlerMiddleware');
|
|
12
|
+
const { getCodeSigningInfoAsync, signManifestString } = require('./build/src/utils/codesigning');
|
|
13
|
+
const { serializeDictionary } = require('structured-headers');
|
|
14
|
+
const { exp, expoGoConfig, bundleUrl, hostUri } = await middleware._resolveProjectSettingsAsync(options);
|
|
15
|
+
const requirements = await resolveRuntimeRequirementsAsync(middleware.projectRoot);
|
|
16
|
+
const signing = await getCodeSigningInfoAsync(exp, options.expectSignature, middleware.options.privateKeyPath);
|
|
17
|
+
const scope = await Manifest.getScopeKeyAsync({ slug: exp.slug, codeSigningInfo: signing });
|
|
18
|
+
|
|
19
|
+
const bundle = new URL(bundleUrl);
|
|
20
|
+
bundle.searchParams.set('transform.engine', 'hermes');
|
|
21
|
+
bundle.searchParams.set('transform.bytecode', '0');
|
|
22
|
+
bundle.searchParams.set('unstable_transformProfile', 'hermes-stable');
|
|
23
|
+
bundle.searchParams.set('lazy', 'false');
|
|
24
|
+
|
|
25
|
+
const name = exp.harmony?.bundleName || (await resolveHarmonyBuildPlanAsync(middleware.projectRoot)).bundleName;
|
|
26
|
+
const id = exp.extra?.eas?.projectId;
|
|
27
|
+
const address = `${bundle.origin}${HarmonyManifestPath}?platform=harmony`;
|
|
28
|
+
const manifest = JSON.stringify({
|
|
29
|
+
id: crypto.randomUUID(),
|
|
30
|
+
createdAt: new Date().toISOString(),
|
|
31
|
+
runtimeVersion: requirements.runtimeVersion,
|
|
32
|
+
launchAsset: { key: 'bundle', contentType: 'application/javascript', url: bundle.toString() },
|
|
33
|
+
assets: [],
|
|
34
|
+
metadata: {},
|
|
35
|
+
extra: {
|
|
36
|
+
eas: { projectId: id ?? undefined },
|
|
37
|
+
expoClient: { ...exp, hostUri },
|
|
38
|
+
expoGo: expoGoConfig,
|
|
39
|
+
scopeKey: scope,
|
|
40
|
+
harmony: {
|
|
41
|
+
projectId: id || `@${exp.owner || 'anonymous'}/${exp.slug}/${name}`,
|
|
42
|
+
manifestUrl: address,
|
|
43
|
+
launchLink: createHarmonyLaunchLink(address),
|
|
44
|
+
requirements,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
const signature = signing
|
|
49
|
+
? serializeDictionary(new Map(Object.entries({
|
|
50
|
+
keyid: signing.keyId,
|
|
51
|
+
sig: signManifestString(manifest, signing),
|
|
52
|
+
alg: 'rsa-v1_5-sha256',
|
|
53
|
+
}).map(([key, value]) => [key, [value, new Map()]])))
|
|
54
|
+
: undefined;
|
|
55
|
+
|
|
56
|
+
if (options.responseContentType === Types.MULTIPART_MIXED) {
|
|
57
|
+
return middleware.encodeFormDataAsync({
|
|
58
|
+
stringifiedManifest: manifest,
|
|
59
|
+
manifestPartHeaders: signature ? { 'expo-signature': signature } : undefined,
|
|
60
|
+
certificateChainBody: signing?.certificateChainForResponse.join('\n') ?? null,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const headers = middleware.getDefaultResponseHeaders();
|
|
65
|
+
headers.set('content-type', Manifest.getContentTypeForResponseContentType(options.responseContentType));
|
|
66
|
+
if (signature) headers.set('expo-signature', signature);
|
|
67
|
+
|
|
68
|
+
return new Response(manifest, { headers });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function installHarmonyManifest(root: string) {
|
|
72
|
+
const expo = createRequire(resolveExpoCli(root).cliPath);
|
|
73
|
+
const require = createRequire(expo.resolve('@expo/cli/package.json'));
|
|
74
|
+
const version = require('./package.json').version;
|
|
75
|
+
if (version !== '55.0.32') {
|
|
76
|
+
throw new HarmonyCliError('ERR_HARMONY_EXPO_CLI_UNSUPPORTED', `The Harmony manifest adapter requires @expo/cli 55.0.32; found ${version}.`, { operation: 'development-manifest' });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { BundlerDevServer } = require('./build/src/start/server/BundlerDevServer');
|
|
80
|
+
const { parsePlatformHeader } = require('./build/src/start/server/middleware/resolvePlatform');
|
|
81
|
+
const create = BundlerDevServer.prototype.getManifestMiddlewareAsync;
|
|
82
|
+
|
|
83
|
+
// Expo installs the manifest before Metro's enhancer; adapt only the created instance.
|
|
84
|
+
BundlerDevServer.prototype.getManifestMiddlewareAsync = async function (this: object, ...args) {
|
|
85
|
+
const middleware = await create.apply(this, args);
|
|
86
|
+
const parse = middleware.getParsedHeaders;
|
|
87
|
+
const respond = middleware._getManifestResponseAsync;
|
|
88
|
+
|
|
89
|
+
middleware.getParsedHeaders = (request) => {
|
|
90
|
+
if (parsePlatformHeader(request) !== 'harmony') return parse.call(middleware, request);
|
|
91
|
+
|
|
92
|
+
// Bypass only the upstream platform assertion without changing the actual request.
|
|
93
|
+
const url = new URL(request.url, 'http://localhost');
|
|
94
|
+
url.searchParams.set('platform', 'ios');
|
|
95
|
+
const copy = Object.create(request);
|
|
96
|
+
copy.url = url.pathname + url.search;
|
|
97
|
+
|
|
98
|
+
return { ...parse.call(middleware, copy), platform: 'harmony' };
|
|
99
|
+
};
|
|
100
|
+
middleware._getManifestResponseAsync = options => options.platform === 'harmony'
|
|
101
|
+
? createManifestResponseAsync(middleware, options, require)
|
|
102
|
+
: respond.call(middleware, options);
|
|
103
|
+
|
|
104
|
+
return middleware;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { HarmonyCliError } from '../errors';
|
|
2
|
+
|
|
3
|
+
export const HarmonyManifestPath = '/manifest';
|
|
4
|
+
|
|
5
|
+
function validateManifestUrl(value: string): URL {
|
|
6
|
+
let url: URL;
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
url = new URL(value);
|
|
10
|
+
} catch (cause) {
|
|
11
|
+
throw new HarmonyCliError(
|
|
12
|
+
'ERR_HARMONY_MANIFEST_URL',
|
|
13
|
+
'Expected an absolute HTTP(S) Harmony manifest URL.',
|
|
14
|
+
{ cause, operation: 'development-manifest' }
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash
|
|
19
|
+
|| !['/', '/manifest', '/index.exp'].includes(url.pathname) || url.searchParams.toString() !== 'platform=harmony') {
|
|
20
|
+
throw new HarmonyCliError(
|
|
21
|
+
'ERR_HARMONY_MANIFEST_URL',
|
|
22
|
+
'Expected an HTTP(S) Expo manifest URL with platform=harmony and without credentials or a fragment.',
|
|
23
|
+
{ operation: 'development-manifest' }
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return url;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createHarmonyLaunchLink(manifest: string): string {
|
|
31
|
+
validateManifestUrl(manifest);
|
|
32
|
+
|
|
33
|
+
const url = new URL('expo-harmony://open');
|
|
34
|
+
url.searchParams.set('url', manifest);
|
|
35
|
+
|
|
36
|
+
return url.toString();
|
|
37
|
+
}
|