@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
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveRuntimeRequirementsAsync = resolveRuntimeRequirementsAsync;
|
|
7
|
+
exports.publishRuntimeContractAsync = publishRuntimeContractAsync;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_module_1 = require("node:module");
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const expo_modules_autolinking_1 = require("@expo-harmony/expo-modules-autolinking");
|
|
12
|
+
const runtime_1 = require("@expo-harmony/expo-modules-autolinking/runtime");
|
|
13
|
+
const json5_1 = __importDefault(require("json5"));
|
|
14
|
+
const export_1 = require("../updates/export");
|
|
15
|
+
const errors_1 = require("../errors");
|
|
16
|
+
const file_1 = require("../file");
|
|
17
|
+
const config_1 = require("./config");
|
|
18
|
+
async function createRuntimeContractAsync(root, project, modules, metro) {
|
|
19
|
+
const resolution = metro ?? await (0, config_1.readMetroRuntimeAsync)(root);
|
|
20
|
+
if (!resolution?.fixedRuntime || resolution.harmonyPackage !== '@react-native-oh/react-native-harmony'
|
|
21
|
+
|| typeof resolution.reactPackage !== 'string' || !resolution.reactPackage) {
|
|
22
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_RUNTIME_CONFIG', 'Runtime contracts require withHarmonyConfig with the standard RNOH package and supported React resolution.', { operation: 'runtime-contract' });
|
|
23
|
+
}
|
|
24
|
+
const require = (0, node_module_1.createRequire)(node_path_1.default.join(root, 'package.json'));
|
|
25
|
+
const version = (name) => require(`${name}/package.json`).version;
|
|
26
|
+
const native = (0, node_module_1.createRequire)(require.resolve(resolution.harmonyPackage + '/package.json'));
|
|
27
|
+
const contract = {
|
|
28
|
+
schemaVersion: 1,
|
|
29
|
+
platform: 'harmony',
|
|
30
|
+
runtimeVersion: project.runtimeVersion,
|
|
31
|
+
development: true,
|
|
32
|
+
expo: version('expo'),
|
|
33
|
+
expoModulesCore: version('expo-modules-core'),
|
|
34
|
+
rnoh: native('./package.json').version,
|
|
35
|
+
react: version(resolution.reactPackage),
|
|
36
|
+
hermes: native('hermes-compiler/package.json').version,
|
|
37
|
+
engine: 'hermes-v1',
|
|
38
|
+
bundleFormat: 'js-source',
|
|
39
|
+
renderer: 'rnoh',
|
|
40
|
+
modules: (0, expo_modules_autolinking_1.createNativeModuleContracts)(modules),
|
|
41
|
+
config: project.native,
|
|
42
|
+
};
|
|
43
|
+
if (contract.rnoh !== '0.84.1' || contract.react !== '19.2.3' || contract.hermes !== '250829098.0.9'
|
|
44
|
+
|| !contract.expo.startsWith('55.') || !modules.some(module => module.packageName === '@expo-harmony/expo-modules-core')) {
|
|
45
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_RUNTIME_UNSUPPORTED', 'The fixed development runtime requires Expo SDK 55, Harmony Expo Modules Core, RNOH 0.84.1, React 19.2.3, and Hermes 250829098.0.9. Check the Metro React alias.', { operation: 'runtime-contract' });
|
|
46
|
+
}
|
|
47
|
+
const issues = (0, runtime_1.validateHarmonyRuntime)(contract);
|
|
48
|
+
if (issues.length > 0) {
|
|
49
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_RUNTIME_CONFIG', issues.map(issue => issue.message).join('\n'), { operation: 'runtime-contract' });
|
|
50
|
+
}
|
|
51
|
+
return contract;
|
|
52
|
+
}
|
|
53
|
+
async function resolveRuntimeRequirementsAsync(root, metro) {
|
|
54
|
+
const result = await (0, expo_modules_autolinking_1.verifyModulesAsync)({ projectRoot: root });
|
|
55
|
+
if (!result.valid) {
|
|
56
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_RUNTIME_MODULES', result.diagnostics.filter(item => item.severity === 'error').map(item => item.message).join('\n'), { operation: 'runtime-contract' });
|
|
57
|
+
}
|
|
58
|
+
const project = await (0, config_1.readProjectRuntimeAsync)(root);
|
|
59
|
+
return createRuntimeContractAsync(root, project, result.modules, metro);
|
|
60
|
+
}
|
|
61
|
+
async function publishRuntimeContractAsync(root, plan) {
|
|
62
|
+
const manifest = JSON.parse(await node_fs_1.default.promises.readFile(node_path_1.default.join(root, '.expo/harmony/autolinking.json'), 'utf8'));
|
|
63
|
+
if (manifest.schemaVersion !== 4 || manifest.platform !== 'harmony' || manifest.buildType !== plan.buildMode) {
|
|
64
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_RUNTIME_BUILD', 'Runtime publication requires autolinking from this native build.', { operation: 'runtime-contract' });
|
|
65
|
+
}
|
|
66
|
+
const project = await (0, config_1.readProjectRuntimeAsync)(root);
|
|
67
|
+
const requirements = await createRuntimeContractAsync(root, project, manifest.modules);
|
|
68
|
+
if (!manifest.runtimeModules || JSON.stringify(requirements.modules) !== JSON.stringify(manifest.runtimeModules)) {
|
|
69
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_RUNTIME_BUILD', 'Native inputs changed or lack a runtime snapshot. Run autolinking/prebuild again before publishing capabilities.', { operation: 'runtime-contract' });
|
|
70
|
+
}
|
|
71
|
+
const read = (file) => json5_1.default.parse(node_fs_1.default.readFileSync(file, 'utf8'));
|
|
72
|
+
const dependencies = read(plan.nativeInputs.manifest);
|
|
73
|
+
const declared = { ...dependencies.dependencies, ...dependencies.overrides };
|
|
74
|
+
const linked = (0, expo_modules_autolinking_1.ohpmDependenciesFromManifest)(manifest, { harmonyProjectPath: plan.harmonyRoot });
|
|
75
|
+
for (const [name, expected] of Object.entries(linked)) {
|
|
76
|
+
const actual = declared[name];
|
|
77
|
+
const target = node_fs_1.default.realpathSync(node_path_1.default.resolve(plan.harmonyRoot, expected));
|
|
78
|
+
if (typeof actual !== 'string' || !actual.startsWith('.')
|
|
79
|
+
|| node_fs_1.default.realpathSync(node_path_1.default.resolve(plan.harmonyRoot, actual)) !== target) {
|
|
80
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_RUNTIME_BUILD', `Native dependency ${name} does not use the HAR recorded by autolinking.`, { operation: 'runtime-contract' });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const directory = node_path_1.default.join(plan.harmonyRoot, 'oh_modules/@rnoh/react-native-openharmony');
|
|
84
|
+
const rnoh = read(node_path_1.default.join(directory, 'oh-package.json5'));
|
|
85
|
+
const hermes = read(node_path_1.default.join(directory, 'src/main/cpp/third-party/hermes/npm/hermes-compiler/package.json'));
|
|
86
|
+
if (rnoh.version !== requirements.rnoh || hermes.version !== requirements.hermes
|
|
87
|
+
|| declared['@rnoh/react-native-openharmony'] !== requirements.rnoh
|
|
88
|
+
|| process.env.HERMES_V1_ENABLED === 'false') {
|
|
89
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_RUNTIME_BUILD', 'Runtime publication requires matching installed RNOH and Hermes v1.', { operation: 'runtime-contract' });
|
|
90
|
+
}
|
|
91
|
+
const native = (0, config_1.readNativeRuntime)(plan);
|
|
92
|
+
requirements.development = plan.buildMode === 'debug';
|
|
93
|
+
const contract = {
|
|
94
|
+
...requirements,
|
|
95
|
+
runtimeVersion: await (0, config_1.resolveRuntimeVersionAsync)(root, project.config, native.app),
|
|
96
|
+
config: native.config,
|
|
97
|
+
};
|
|
98
|
+
(0, runtime_1.assertHarmonyCompatibility)(contract, requirements);
|
|
99
|
+
const require = (0, node_module_1.createRequire)(node_path_1.default.join(root, 'package.json'));
|
|
100
|
+
const file = node_path_1.default.join(plan.exportPaths.rawfileRoot, 'expo-harmony-runtime.json');
|
|
101
|
+
const source = require.resolve('@expo-harmony/expo-modules-autolinking/runtime-source');
|
|
102
|
+
const target = node_path_1.default.join(plan.moduleRoot, 'src/main/ets/generated/HarmonyRuntime.ts');
|
|
103
|
+
await (0, file_1.atomicCopy)(source, target, root);
|
|
104
|
+
await (0, file_1.atomicWriteJson)(contract, file, root);
|
|
105
|
+
await (0, file_1.atomicWriteJson)(contract, node_path_1.default.join(root, '.expo/harmony/runtime.json'), root);
|
|
106
|
+
await (0, export_1.publishUpdatesConfigurationAsync)(root, plan.exportPaths.rawfileRoot, project.config, contract.runtimeVersion);
|
|
107
|
+
return contract;
|
|
108
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function fingerprintHarmonyAsync(root: string): Promise<import("@expo/fingerprint").Fingerprint>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.fingerprintHarmonyAsync = fingerprintHarmonyAsync;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const node_module_1 = require("node:module");
|
|
10
|
+
const fingerprint_1 = require("@expo/fingerprint");
|
|
11
|
+
const expo_modules_autolinking_1 = require("@expo-harmony/expo-modules-autolinking");
|
|
12
|
+
const errors_1 = require("../errors");
|
|
13
|
+
async function fingerprintHarmonyAsync(root) {
|
|
14
|
+
const result = await (0, expo_modules_autolinking_1.verifyModulesAsync)({ projectRoot: root });
|
|
15
|
+
if (!result.valid)
|
|
16
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_FINGERPRINT', 'Build and verify Harmony native modules before calculating the runtime fingerprint.', { operation: 'fingerprint' });
|
|
17
|
+
const require = (0, node_module_1.createRequire)(node_path_1.default.join(root, 'package.json'));
|
|
18
|
+
const dependencies = (0, expo_modules_autolinking_1.createNativeModuleContracts)(result.modules);
|
|
19
|
+
const generated = [];
|
|
20
|
+
const previous = node_path_1.default.join(root, '.expo/harmony/export-manifest.json');
|
|
21
|
+
if (node_fs_1.default.existsSync(previous)) {
|
|
22
|
+
const manifest = JSON.parse(node_fs_1.default.readFileSync(previous, 'utf8'));
|
|
23
|
+
generated.push(...manifest.assets.map(asset => `**/harmony/**/src/main/resources/rawfile/${asset.path}`));
|
|
24
|
+
}
|
|
25
|
+
const sources = [{ type: 'contents', id: 'harmonyAutolinking', contents: JSON.stringify(dependencies), reasons: ['harmonyAutolinking'] }];
|
|
26
|
+
for (const name of ['@react-native-oh/react-native-harmony', '@expo-harmony/template', '@expo-harmony/config-plugins', '@expo-harmony/prebuild-config']) {
|
|
27
|
+
const file = require.resolve(name + '/package.json');
|
|
28
|
+
sources.push({ type: 'dir', filePath: node_path_1.default.relative(root, node_path_1.default.dirname(file)), reasons: ['harmonyNativeToolchain'] });
|
|
29
|
+
}
|
|
30
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(root, 'harmony')))
|
|
31
|
+
sources.push({ type: 'dir', filePath: 'harmony', reasons: ['harmonyNativeProject'] });
|
|
32
|
+
return (0, fingerprint_1.createFingerprintAsync)(root, {
|
|
33
|
+
platforms: [], extraSources: sources,
|
|
34
|
+
ignorePaths: [
|
|
35
|
+
...generated,
|
|
36
|
+
'**/harmony/**/build/**', '**/harmony/**/.hvigor/**', '**/harmony/**/.cxx/**', '**/harmony/**/oh_modules/**',
|
|
37
|
+
'**/harmony/**/oh-package-lock.json5', '**/harmony/**/local.properties', '**/harmony/**/native-inputs.cmake',
|
|
38
|
+
'**/harmony/**/HarmonyRuntime.ts', '**/harmony/**/expo-harmony-runtime.json', '**/harmony/**/expo-updates/**',
|
|
39
|
+
'**/harmony/**/hermes_bundle.hbc', '**/harmony/**/hermes_bundle.hbc.*', '**/harmony/**/.expo-harmony/**',
|
|
40
|
+
'**/harmony/**/autolinking.cmake', '**/harmony/**/autolinking.cpp',
|
|
41
|
+
'**/harmony/**/generated/**', '**/*.har', '**/*.hap', '**/.git/**', '**/node_modules/**/node_modules/**',
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.command = void 0;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const runtime_1 = require("@expo-harmony/expo-modules-autolinking/runtime");
|
|
10
|
+
const args_1 = require("../args");
|
|
11
|
+
const command_1 = require("../command");
|
|
12
|
+
const errors_1 = require("../errors");
|
|
13
|
+
const project_1 = require("../native/project");
|
|
14
|
+
const contract_1 = require("./contract");
|
|
15
|
+
const command = async (argv, io) => {
|
|
16
|
+
const args = (0, command_1.parseCommandArgs)((argv) => {
|
|
17
|
+
const { values, positionals } = (0, args_1.parseArgs)({
|
|
18
|
+
...args_1.CommonOptions,
|
|
19
|
+
publish: { type: 'boolean' },
|
|
20
|
+
client: { type: 'string' },
|
|
21
|
+
variant: { type: 'string' },
|
|
22
|
+
}, argv);
|
|
23
|
+
if (positionals.length > 1 || (values.variant && !['debug', 'release'].includes(values.variant))
|
|
24
|
+
|| (values.publish && values.client)) {
|
|
25
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', 'Use runtime [project] [--client file | --publish] [--variant debug|release].', { operation: 'parse-arguments' });
|
|
26
|
+
}
|
|
27
|
+
return { help: Boolean(values.help), project: positionals[0], ...values };
|
|
28
|
+
}, argv, io);
|
|
29
|
+
if (!args)
|
|
30
|
+
return 0;
|
|
31
|
+
const { projectRoot: root, options } = args;
|
|
32
|
+
if (options.publish) {
|
|
33
|
+
const plan = await (0, project_1.resolveHarmonyBuildPlanAsync)(root, { buildMode: options.variant });
|
|
34
|
+
await (0, contract_1.publishRuntimeContractAsync)(root, plan);
|
|
35
|
+
io.log('Published Harmony runtime capabilities and host validator.');
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const contract = await (0, contract_1.resolveRuntimeRequirementsAsync)(root);
|
|
39
|
+
if (options.client) {
|
|
40
|
+
const client = JSON.parse(await node_fs_1.default.promises.readFile(node_path_1.default.resolve(options.client), 'utf8'));
|
|
41
|
+
(0, runtime_1.assertHarmonyCompatibility)(client, contract);
|
|
42
|
+
io.log('Project is compatible with the fixed Harmony client.');
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
io.log(JSON.stringify(contract, null, 2));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return 0;
|
|
49
|
+
};
|
|
50
|
+
exports.command = command;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.command = void 0;
|
|
4
|
+
const command_1 = require("../command");
|
|
5
|
+
const options_1 = require("./options");
|
|
6
|
+
const command = async (argv, io) => {
|
|
7
|
+
const args = (0, command_1.parseCommandArgs)(options_1.parseStartArgs, argv, io);
|
|
8
|
+
if (!args)
|
|
9
|
+
return 0;
|
|
10
|
+
const { projectRoot: root, options } = args;
|
|
11
|
+
const { startExpoMetroAsync } = await import('../run/metro.js');
|
|
12
|
+
const metro = await startExpoMetroAsync(root, {
|
|
13
|
+
...options,
|
|
14
|
+
interactive: true,
|
|
15
|
+
});
|
|
16
|
+
try {
|
|
17
|
+
if (metro.development) {
|
|
18
|
+
io.log(`Harmony manifest: ${metro.development.manifestUrl}`);
|
|
19
|
+
io.log(`Harmony JS bundle: ${metro.development.bundleUrl}`);
|
|
20
|
+
io.log(`Harmony launch link: ${metro.development.launchLink}`);
|
|
21
|
+
}
|
|
22
|
+
if (metro.owner === 'existing') {
|
|
23
|
+
io.log(`Expo Metro is already running on port ${metro.port}.`);
|
|
24
|
+
if (options.resetCache) {
|
|
25
|
+
io.warn('Stop the existing Metro server and run this command again to reset its cache.');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
io.log('\n› Logs for your project will appear below. Press Ctrl+C to exit.');
|
|
30
|
+
await metro.waitAsync();
|
|
31
|
+
}
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await metro.stop();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
exports.command = command;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseStartArgs = parseStartArgs;
|
|
4
|
+
const args_1 = require("../args");
|
|
5
|
+
const errors_1 = require("../errors");
|
|
6
|
+
const StartOptions = {
|
|
7
|
+
...args_1.CommonOptions,
|
|
8
|
+
'clear': { short: 'c', type: 'boolean' },
|
|
9
|
+
'host': { type: 'string' },
|
|
10
|
+
'port': { type: 'string' },
|
|
11
|
+
'private-key-path': { type: 'string' },
|
|
12
|
+
'reset-cache': { type: 'boolean' },
|
|
13
|
+
};
|
|
14
|
+
function parseStartArgs(argv) {
|
|
15
|
+
const { positionals, values } = (0, args_1.parseArgs)(StartOptions, argv);
|
|
16
|
+
if (positionals.length > 1) {
|
|
17
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', `Unexpected start positional argument: ${positionals[1]}`, {
|
|
18
|
+
operation: 'parse-arguments',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const port = values.port === undefined ? 8081 : Number(values.port);
|
|
22
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
23
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--port must be an integer between 1 and 65535.', {
|
|
24
|
+
operation: 'parse-arguments',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (values.host !== undefined && !/^[A-Za-z0-9.-]+$/.test(values.host)) {
|
|
28
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--host must be a hostname or IPv4 address without a port or scheme.', { operation: 'parse-arguments' });
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
help: Boolean(values.help),
|
|
32
|
+
host: values.host,
|
|
33
|
+
port,
|
|
34
|
+
privateKeyPath: values['private-key-path'],
|
|
35
|
+
project: positionals[0],
|
|
36
|
+
resetCache: Boolean(values['reset-cache'] || values.clear),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ExpoConfigWithHarmony } from '@expo-harmony/config-plugins';
|
|
2
|
+
export interface UpdateAsset {
|
|
3
|
+
key: string;
|
|
4
|
+
hash: string;
|
|
5
|
+
contentType: string;
|
|
6
|
+
fileExtension: string;
|
|
7
|
+
embeddedAssetFilename: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function publishUpdatesConfigurationAsync(root: string, directory: string, config: ExpoConfigWithHarmony, runtime: string): Promise<void>;
|
|
10
|
+
export declare function publishEmbeddedUpdateAsync(root: string, paths: {
|
|
11
|
+
rawfileRoot: string;
|
|
12
|
+
bundle: string;
|
|
13
|
+
metadataRoot: string;
|
|
14
|
+
}, assets: UpdateAsset[]): Promise<void>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.publishUpdatesConfigurationAsync = publishUpdatesConfigurationAsync;
|
|
7
|
+
exports.publishEmbeddedUpdateAsync = publishEmbeddedUpdateAsync;
|
|
8
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
9
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const file_1 = require("../file");
|
|
12
|
+
const errors_1 = require("../errors");
|
|
13
|
+
const config_1 = require("../runtime/config");
|
|
14
|
+
async function publishUpdatesConfigurationAsync(root, directory, config, runtime) {
|
|
15
|
+
const updates = { ...config.updates, runtimeVersion: runtime };
|
|
16
|
+
if (updates.url) {
|
|
17
|
+
const url = new URL(updates.url);
|
|
18
|
+
if (!['http:', 'https:'].includes(url.protocol))
|
|
19
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_UPDATES_CONFIG', 'Updates URL must use HTTP or HTTPS.', { operation: 'updates-config' });
|
|
20
|
+
}
|
|
21
|
+
if (updates.codeSigningCertificate) {
|
|
22
|
+
const certificate = await promises_1.default.readFile(node_path_1.default.resolve(root, updates.codeSigningCertificate), 'utf8');
|
|
23
|
+
new node_crypto_1.default.X509Certificate(certificate);
|
|
24
|
+
updates.codeSigningCertificate = certificate;
|
|
25
|
+
}
|
|
26
|
+
await (0, file_1.atomicWriteJson)(updates, node_path_1.default.join(directory, 'expo-updates/config.json'), directory);
|
|
27
|
+
}
|
|
28
|
+
async function publishEmbeddedUpdateAsync(root, paths, assets) {
|
|
29
|
+
const project = await (0, config_1.readProjectRuntimeAsync)(root);
|
|
30
|
+
const bundle = await promises_1.default.readFile(paths.bundle);
|
|
31
|
+
const hash = node_crypto_1.default.createHash('sha256').update(Uint8Array.from(bundle)).digest('base64url');
|
|
32
|
+
const directory = node_path_1.default.join(paths.rawfileRoot, 'expo-updates');
|
|
33
|
+
const id = node_crypto_1.default.randomUUID();
|
|
34
|
+
const manifest = {
|
|
35
|
+
id, commitTime: Date.now(),
|
|
36
|
+
launchAsset: { packagerHash: `bundle-${id}`, hash, type: 'hbc',
|
|
37
|
+
embeddedAssetFilename: node_path_1.default.relative(paths.rawfileRoot, paths.bundle).split(node_path_1.default.sep).join('/') },
|
|
38
|
+
assets: [...new Map(assets.map(asset => [asset.key, asset])).values()].map(asset => ({
|
|
39
|
+
packagerHash: asset.key, hash: asset.hash, type: asset.fileExtension.replace(/^\./, ''),
|
|
40
|
+
embeddedAssetFilename: asset.embeddedAssetFilename,
|
|
41
|
+
})),
|
|
42
|
+
};
|
|
43
|
+
await publishUpdatesConfigurationAsync(root, paths.rawfileRoot, project.config, project.runtimeVersion);
|
|
44
|
+
await (0, file_1.atomicWriteJson)(manifest, node_path_1.default.join(directory, 'manifest.json'), paths.rawfileRoot);
|
|
45
|
+
await (0, file_1.atomicWriteJson)(manifest, node_path_1.default.join(paths.metadataRoot, 'update.json'), paths.metadataRoot);
|
|
46
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.command = void 0;
|
|
7
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
8
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
9
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
10
|
+
const node_module_1 = require("node:module");
|
|
11
|
+
const config_1 = require("@expo/config");
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const args_1 = require("../args");
|
|
14
|
+
const command_1 = require("../command");
|
|
15
|
+
const export_1 = require("../exportEmbed/export");
|
|
16
|
+
const errors_1 = require("../errors");
|
|
17
|
+
const file_1 = require("../file");
|
|
18
|
+
const path_1 = require("../path");
|
|
19
|
+
const expo_1 = require("../expo");
|
|
20
|
+
const config_2 = require("../runtime/config");
|
|
21
|
+
const command = async (argv, io) => {
|
|
22
|
+
const args = (0, command_1.parseCommandArgs)((values) => {
|
|
23
|
+
const { values: options, positionals } = (0, args_1.parseArgs)({ ...args_1.CommonOptions,
|
|
24
|
+
'output-dir': { type: 'string' }, 'asset-url': { type: 'string' },
|
|
25
|
+
'private-key-path': { type: 'string' }, 'reset-cache': { type: 'boolean' },
|
|
26
|
+
}, values);
|
|
27
|
+
if (positionals.length > 1)
|
|
28
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_EXPORT_ARGUMENTS', 'Expected at most one project directory.', { operation: 'export' });
|
|
29
|
+
return { ...options, project: positionals[0], help: Boolean(options.help) };
|
|
30
|
+
}, argv, io);
|
|
31
|
+
if (!args)
|
|
32
|
+
return 0;
|
|
33
|
+
const { projectRoot: root, options } = args;
|
|
34
|
+
const base = options['asset-url'];
|
|
35
|
+
if (!base || !['http:', 'https:'].includes(new URL(base).protocol)) {
|
|
36
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_EXPORT_ARGUMENTS', 'OTA export requires --asset-url with the HTTP(S) directory that will host its assets.', { operation: 'export' });
|
|
37
|
+
}
|
|
38
|
+
const output = node_path_1.default.resolve(root, options['output-dir'] ?? 'dist');
|
|
39
|
+
const temporary = await promises_1.default.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'expo-harmony-update-'));
|
|
40
|
+
try {
|
|
41
|
+
await (0, export_1.exportEmbedAsync)(root, { outputDirectory: temporary, resetCache: options['reset-cache'] });
|
|
42
|
+
const embedded = JSON.parse(await promises_1.default.readFile(node_path_1.default.join(temporary, 'update.json'), 'utf8'));
|
|
43
|
+
const project = await (0, config_2.readProjectRuntimeAsync)(root);
|
|
44
|
+
const { exp } = (0, config_1.getConfig)(root, { isPublicConfig: true });
|
|
45
|
+
const require = (0, node_module_1.createRequire)((0, expo_1.resolveExpoCli)(root).cliPath);
|
|
46
|
+
const upstream = (0, node_module_1.createRequire)(require.resolve('@expo/cli/package.json'));
|
|
47
|
+
const mime = upstream('mime-types');
|
|
48
|
+
const assets = [embedded.launchAsset, ...embedded.assets].map(asset => ({
|
|
49
|
+
key: asset.packagerHash, hash: asset.hash,
|
|
50
|
+
contentType: asset.type === 'hbc' ? 'application/javascript' : mime.lookup(asset.type) || 'application/octet-stream',
|
|
51
|
+
fileExtension: asset.type ? '.' + asset.type : '', embeddedAssetFilename: asset.embeddedAssetFilename,
|
|
52
|
+
}));
|
|
53
|
+
const manifest = {
|
|
54
|
+
id: embedded.id, createdAt: new Date(embedded.commitTime).toISOString(), runtimeVersion: project.runtimeVersion,
|
|
55
|
+
launchAsset: assets[0], assets: assets.slice(1), metadata: {},
|
|
56
|
+
extra: { expoClient: exp, eas: { projectId: exp.extra?.eas?.projectId },
|
|
57
|
+
scopeKey: exp.originalFullName ?? `@${exp.owner ?? 'anonymous'}/${exp.slug}` },
|
|
58
|
+
};
|
|
59
|
+
const metadata = [];
|
|
60
|
+
for (const asset of assets) {
|
|
61
|
+
const relative = `assets/${asset.hash}${asset.fileExtension}`;
|
|
62
|
+
await (0, file_1.atomicCopy)(node_path_1.default.join(temporary, 'embedded', (0, path_1.assertSafeRelative)(asset.embeddedAssetFilename, 'Embedded update asset')), node_path_1.default.join(output, relative), output);
|
|
63
|
+
Object.assign(asset, { url: new URL(relative, base.endsWith('/') ? base : base + '/').href });
|
|
64
|
+
delete asset.embeddedAssetFilename;
|
|
65
|
+
if (asset !== manifest.launchAsset)
|
|
66
|
+
metadata.push({ path: relative, ext: asset.fileExtension.replace(/^\./, '') });
|
|
67
|
+
}
|
|
68
|
+
await (0, file_1.atomicCopy)(node_path_1.default.join(temporary, 'hermes_bundle.hbc.map'), node_path_1.default.join(output, 'bundle.hbc.map'), output);
|
|
69
|
+
await (0, file_1.atomicWriteJson)({ version: 0, bundler: 'metro', fileMetadata: { harmony: {
|
|
70
|
+
bundle: `assets/${manifest.launchAsset.hash}.hbc`, assets: metadata,
|
|
71
|
+
} } }, node_path_1.default.join(output, 'metadata.json'), output);
|
|
72
|
+
await (0, file_1.atomicWriteJson)(manifest, node_path_1.default.join(output, 'manifest.json'), output);
|
|
73
|
+
if (options['private-key-path']) {
|
|
74
|
+
const key = await promises_1.default.readFile(node_path_1.default.resolve(root, options['private-key-path']), 'utf8');
|
|
75
|
+
if (node_crypto_1.default.createPrivateKey(key).asymmetricKeyType !== 'rsa')
|
|
76
|
+
throw new errors_1.HarmonyCliError('ERR_HARMONY_UPDATES_SIGNING', 'Expo Updates signing requires an RSA private key.', { operation: 'export' });
|
|
77
|
+
const body = await promises_1.default.readFile(node_path_1.default.join(output, 'manifest.json'));
|
|
78
|
+
const signature = node_crypto_1.default.sign('RSA-SHA256', Uint8Array.from(body), key).toString('base64');
|
|
79
|
+
const id = exp.updates?.codeSigningMetadata?.keyid ?? 'root';
|
|
80
|
+
const { serializeDictionary } = upstream('structured-headers');
|
|
81
|
+
await promises_1.default.writeFile(node_path_1.default.join(temporary, 'signature.txt'), serializeDictionary(new Map([
|
|
82
|
+
['sig', [signature, new Map()]], ['keyid', [id, new Map()]], ['alg', ['rsa-v1_5-sha256', new Map()]],
|
|
83
|
+
])));
|
|
84
|
+
await (0, file_1.atomicCopy)(node_path_1.default.join(temporary, 'signature.txt'), node_path_1.default.join(output, 'signature.txt'), output);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
await promises_1.default.rm(node_path_1.default.join(output, 'signature.txt'), { force: true });
|
|
88
|
+
}
|
|
89
|
+
io.log(`Exported Harmony update ${manifest.id} for runtime ${manifest.runtimeVersion} to ${output}.`);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await promises_1.default.rm(temporary, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
return 0;
|
|
95
|
+
};
|
|
96
|
+
exports.command = command;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
declare const ProjectPackages: Readonly<{
|
|
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
|
+
declare const RequiredProjectPackages: readonly ("@react-native-oh/react-native-harmony" | "@expo-harmony/expo-modules-autolinking" | "@react-native-oh/react-native-harmony-cli")[];
|
|
9
|
+
export { ProjectPackages, RequiredProjectPackages, };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RequiredProjectPackages = exports.ProjectPackages = void 0;
|
|
4
|
+
const ProjectPackages = Object.freeze({
|
|
5
|
+
expo: 'expo',
|
|
6
|
+
expoAutolinking: '@expo-harmony/expo-modules-autolinking',
|
|
7
|
+
reactNative: 'react-native',
|
|
8
|
+
rnohCli: '@react-native-oh/react-native-harmony-cli',
|
|
9
|
+
rnohRuntime: '@react-native-oh/react-native-harmony',
|
|
10
|
+
});
|
|
11
|
+
exports.ProjectPackages = ProjectPackages;
|
|
12
|
+
const RequiredProjectPackages = Object.freeze([
|
|
13
|
+
ProjectPackages.rnohRuntime,
|
|
14
|
+
ProjectPackages.rnohCli,
|
|
15
|
+
ProjectPackages.expoAutolinking,
|
|
16
|
+
]);
|
|
17
|
+
exports.RequiredProjectPackages = RequiredProjectPackages;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo-harmony/cli",
|
|
3
|
-
"version": "55.0.26-harmony.
|
|
3
|
+
"version": "55.0.26-harmony.10",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"react-native",
|
|
6
6
|
"expo",
|
|
@@ -30,16 +30,32 @@
|
|
|
30
30
|
"require": "./build/index.js",
|
|
31
31
|
"default": "./build/index.js"
|
|
32
32
|
},
|
|
33
|
-
"./bin/expo-harmony": "./build/bin/expo-harmony.js"
|
|
33
|
+
"./bin/expo-harmony": "./build/bin/expo-harmony.js",
|
|
34
|
+
"./native-build": {
|
|
35
|
+
"types": "./build/native/hvigor.d.ts",
|
|
36
|
+
"require": "./build/native/hvigor.js",
|
|
37
|
+
"default": "./build/native/hvigor.js"
|
|
38
|
+
},
|
|
39
|
+
"./internal/prebuild": {
|
|
40
|
+
"types": "./build/internal/prebuild.d.ts",
|
|
41
|
+
"require": "./build/internal/prebuild.js",
|
|
42
|
+
"default": "./build/internal/prebuild.js"
|
|
43
|
+
},
|
|
44
|
+
"./internal/development": {
|
|
45
|
+
"types": "./build/internal/development.d.ts",
|
|
46
|
+
"require": "./build/internal/development.js",
|
|
47
|
+
"default": "./build/internal/development.js"
|
|
48
|
+
}
|
|
34
49
|
},
|
|
35
50
|
"bin": {
|
|
36
51
|
"expo-harmony": "build/bin/expo-harmony.js"
|
|
37
52
|
},
|
|
38
53
|
"dependencies": {
|
|
39
|
-
"@expo-harmony/config-plugins": "55.0.10-harmony.
|
|
40
|
-
"@expo-harmony/expo-modules-autolinking": "55.0.25-harmony.
|
|
41
|
-
"@expo-harmony/prebuild-config": "55.0.0-harmony.
|
|
54
|
+
"@expo-harmony/config-plugins": "55.0.10-harmony.4",
|
|
55
|
+
"@expo-harmony/expo-modules-autolinking": "55.0.25-harmony.4",
|
|
56
|
+
"@expo-harmony/prebuild-config": "55.0.0-harmony.8",
|
|
42
57
|
"@expo/config": "55.0.17",
|
|
58
|
+
"@expo/fingerprint": "0.16.7",
|
|
43
59
|
"cross-spawn": "^7.0.6",
|
|
44
60
|
"json5": "2.2.3"
|
|
45
61
|
},
|
package/src/bin/expo-harmony.ts
CHANGED