@expo-harmony/cli 55.0.26-harmony.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -0
- package/build/bin/expo-harmony.js +5 -0
- package/build/index.js +22 -0
- package/package.json +67 -0
- package/src/args.ts +42 -0
- package/src/bin/expo-harmony.ts +5 -0
- package/src/buildHap/build.ts +164 -0
- package/src/buildHap/common.ts +145 -0
- package/src/buildHap/options.ts +35 -0
- package/src/cli.ts +242 -0
- package/src/doctor/doctor.ts +267 -0
- package/src/doctor/options.ts +17 -0
- package/src/entry.ts +47 -0
- package/src/errors.ts +22 -0
- package/src/expo.ts +89 -0
- package/src/exportEmbed/export.ts +177 -0
- package/src/exportEmbed/manifest.ts +222 -0
- package/src/exportEmbed/options.ts +25 -0
- package/src/file.ts +110 -0
- package/src/index.ts +12 -0
- package/src/modules/modules.ts +154 -0
- package/src/modules/options.ts +70 -0
- package/src/path.ts +27 -0
- package/src/prebuild/check.ts +189 -0
- package/src/prebuild/clean.ts +67 -0
- package/src/prebuild/options.ts +69 -0
- package/src/prebuild/prebuild.ts +80 -0
- package/src/prebuild/template.ts +119 -0
- package/src/process.ts +275 -0
- package/src/project.ts +21 -0
- package/src/projectLock.ts +229 -0
- package/src/run/cache.ts +105 -0
- package/src/run/devices.ts +302 -0
- package/src/run/emulators.ts +107 -0
- package/src/run/install.ts +41 -0
- package/src/run/metro.ts +237 -0
- package/src/run/options.ts +71 -0
- package/src/run/run.ts +302 -0
- package/src/start/options.ts +36 -0
- package/src/tools.ts +316 -0
- package/src/upstream.ts +18 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { doctorAsync, type DoctorResult } from '../doctor/doctor';
|
|
7
|
+
import { resolveHarmonyEntryPoint } from '../entry';
|
|
8
|
+
import { HarmonyCliError } from '../errors';
|
|
9
|
+
import { withHarmonyProjectLockAsync } from '../projectLock';
|
|
10
|
+
import { resolveHarmonyBuildPlanAsync } from '../tools';
|
|
11
|
+
import { formatDiagnostics, spawnAsync } from '../process';
|
|
12
|
+
import { resolveExpoCli, resolveExpoHermesBuilder } from '../expo';
|
|
13
|
+
import {
|
|
14
|
+
assertHermesBundle, assertSourceMap, exportPaths,
|
|
15
|
+
publishExportAsync, validatePublishedExportAsync,
|
|
16
|
+
type HarmonyExportManifest,
|
|
17
|
+
} from './manifest';
|
|
18
|
+
|
|
19
|
+
export interface ExportOptions {
|
|
20
|
+
check?: boolean;
|
|
21
|
+
resetCache?: boolean;
|
|
22
|
+
skipDoctor?: boolean;
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ExportTemporary {
|
|
27
|
+
assets: string;
|
|
28
|
+
bundle: string;
|
|
29
|
+
javascript: string;
|
|
30
|
+
metroSourceMap: string;
|
|
31
|
+
sourceMap: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createExpoExportEmbedArgs(
|
|
35
|
+
projectRoot: string,
|
|
36
|
+
entryFile: string,
|
|
37
|
+
temporary: ExportTemporary,
|
|
38
|
+
options: ExportOptions = {}
|
|
39
|
+
) {
|
|
40
|
+
return [
|
|
41
|
+
'export:embed',
|
|
42
|
+
'--platform', 'harmony',
|
|
43
|
+
'--entry-file', entryFile,
|
|
44
|
+
'--bundle-output', temporary.javascript,
|
|
45
|
+
'--assets-dest', temporary.assets,
|
|
46
|
+
'--dev', 'false',
|
|
47
|
+
'--minify', 'false',
|
|
48
|
+
'--sourcemap-output', temporary.metroSourceMap,
|
|
49
|
+
'--sourcemap-sources-root', '.',
|
|
50
|
+
'--unstable-transform-profile', 'hermes-stable',
|
|
51
|
+
...(options.resetCache ? ['--reset-cache=true'] : []),
|
|
52
|
+
projectRoot,
|
|
53
|
+
];
|
|
54
|
+
}
|
|
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
|
+
async function exportEmbedUnlockedAsync(
|
|
64
|
+
projectRoot: string,
|
|
65
|
+
options: ExportOptions = {}
|
|
66
|
+
): Promise<HarmonyExportManifest> {
|
|
67
|
+
if (!options.skipDoctor) assertDoctor(await doctorAsync(projectRoot));
|
|
68
|
+
|
|
69
|
+
const plan = await resolveHarmonyBuildPlanAsync(projectRoot, { buildMode: 'release' });
|
|
70
|
+
const paths = exportPaths(plan);
|
|
71
|
+
|
|
72
|
+
if (options.check) return await validatePublishedExportAsync(paths);
|
|
73
|
+
|
|
74
|
+
const entryFile = resolveHarmonyEntryPoint(projectRoot);
|
|
75
|
+
const temporaryRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'expo-harmony-export-'));
|
|
76
|
+
const temporary: ExportTemporary = {
|
|
77
|
+
assets: path.join(temporaryRoot, 'assets'),
|
|
78
|
+
bundle: path.join(temporaryRoot, 'hermes_bundle.hbc'),
|
|
79
|
+
javascript: path.join(temporaryRoot, 'index.js'),
|
|
80
|
+
metroSourceMap: path.join(temporaryRoot, 'index.js.map'),
|
|
81
|
+
sourceMap: path.join(temporaryRoot, 'hermes_bundle.hbc.map'),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await fs.promises.mkdir(temporary.assets, { recursive: true });
|
|
86
|
+
|
|
87
|
+
const expo = resolveExpoCli(projectRoot);
|
|
88
|
+
const result = await spawnAsync(process.execPath, [
|
|
89
|
+
expo.cliPath,
|
|
90
|
+
...createExpoExportEmbedArgs(projectRoot, entryFile, temporary, options),
|
|
91
|
+
], {
|
|
92
|
+
capture: true,
|
|
93
|
+
cwd: projectRoot,
|
|
94
|
+
env: {
|
|
95
|
+
...process.env,
|
|
96
|
+
EXPO_METRO_TARGET: 'harmony',
|
|
97
|
+
HERMES_V1_ENABLED: 'true',
|
|
98
|
+
NODE_ENV: 'production',
|
|
99
|
+
},
|
|
100
|
+
operation: 'expo-export-embed',
|
|
101
|
+
outputLimit: 4 * 1024 * 1024,
|
|
102
|
+
timeoutMs: options.timeoutMs || 10 * 60_000,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (result.code !== 0 || result.timedOut) {
|
|
106
|
+
const diagnostics = formatDiagnostics(result);
|
|
107
|
+
throw new HarmonyCliError(
|
|
108
|
+
'ERR_HARMONY_EXPORT_FAILED',
|
|
109
|
+
`Expo export:embed exited with code ${result.code}${result.timedOut ? ' after timing out' : ''}.${diagnostics ? `\n${diagnostics}` : ''}`,
|
|
110
|
+
{ exitCode: result.code || 1, operation: 'expo-export-embed' }
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Expo 55 only infers Hermes for iOS and Android. Keep Harmony bundling in the Expo CLI,
|
|
115
|
+
// then explicitly hand its JS and source map to Expo's own Hermes exporter.
|
|
116
|
+
try {
|
|
117
|
+
const [code, map] = await Promise.all([
|
|
118
|
+
fs.promises.readFile(temporary.javascript, 'utf8'),
|
|
119
|
+
fs.promises.readFile(temporary.metroSourceMap, 'utf8'),
|
|
120
|
+
]);
|
|
121
|
+
const buildHermesBundleAsync = resolveExpoHermesBuilder(projectRoot);
|
|
122
|
+
// Expo resolves hermes-compiler from react-native. For Harmony, resolve it
|
|
123
|
+
// from RNOH instead of the app's Android/iOS React Native installation.
|
|
124
|
+
const projectRequire = createRequire(path.join(projectRoot, 'package.json'));
|
|
125
|
+
const harmonyRuntime = path.dirname(projectRequire.resolve('@react-native-oh/react-native-harmony/package.json'));
|
|
126
|
+
const compilerModules = path.join(temporaryRoot, 'node_modules');
|
|
127
|
+
await fs.promises.mkdir(compilerModules);
|
|
128
|
+
await fs.promises.symlink(harmonyRuntime, path.join(compilerModules, 'react-native'),
|
|
129
|
+
process.platform === 'win32' ? 'junction' : 'dir');
|
|
130
|
+
const output = await buildHermesBundleAsync({
|
|
131
|
+
code,
|
|
132
|
+
filename: entryFile,
|
|
133
|
+
map,
|
|
134
|
+
minify: true,
|
|
135
|
+
projectRoot: temporaryRoot,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (!(output?.hbc instanceof Uint8Array) || typeof output.sourcemap !== 'string') {
|
|
139
|
+
throw new Error('Expo did not return Hermes bytecode and a composed source map.');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await Promise.all([
|
|
143
|
+
fs.promises.writeFile(temporary.bundle, output.hbc),
|
|
144
|
+
fs.promises.writeFile(temporary.sourceMap, output.sourcemap),
|
|
145
|
+
]);
|
|
146
|
+
} catch (cause) {
|
|
147
|
+
if (cause instanceof HarmonyCliError) throw cause;
|
|
148
|
+
throw new HarmonyCliError(
|
|
149
|
+
'ERR_HARMONY_EXPORT_HERMES',
|
|
150
|
+
'Expo failed to compile the Harmony bundle to Hermes bytecode.',
|
|
151
|
+
{ cause, operation: 'expo-hermes-export' }
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const bytecode = await assertHermesBundle(temporary.bundle);
|
|
156
|
+
await assertSourceMap(temporary.sourceMap);
|
|
157
|
+
|
|
158
|
+
return await publishExportAsync(projectRoot, paths, temporary, entryFile, bytecode);
|
|
159
|
+
} finally {
|
|
160
|
+
await fs.promises.rm(temporaryRoot, { force: true, recursive: true });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function exportEmbedAsync(
|
|
165
|
+
projectRoot: string,
|
|
166
|
+
options: ExportOptions = {}
|
|
167
|
+
): Promise<HarmonyExportManifest> {
|
|
168
|
+
return withHarmonyProjectLockAsync(
|
|
169
|
+
projectRoot,
|
|
170
|
+
'export:embed',
|
|
171
|
+
() => exportEmbedUnlockedAsync(projectRoot, options)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export {
|
|
176
|
+
exportEmbedAsync,
|
|
177
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { HarmonyCliError } from '../errors';
|
|
6
|
+
import { atomicCopy, atomicWriteJson, describeFile, listFiles, removeEmptyParents } from '../file';
|
|
7
|
+
import { assertSafeRelative, isInside, toPosixPath } from '../path';
|
|
8
|
+
import type { ExportTemporary } from './export';
|
|
9
|
+
|
|
10
|
+
export interface HarmonyExportFile {
|
|
11
|
+
path: string;
|
|
12
|
+
sha256: string;
|
|
13
|
+
size: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface HarmonyExportManifest {
|
|
17
|
+
assets: HarmonyExportFile[];
|
|
18
|
+
bundle: HarmonyExportFile & { bytecodeVersion: number };
|
|
19
|
+
entryFile: string;
|
|
20
|
+
platform: 'harmony';
|
|
21
|
+
schemaVersion: 1;
|
|
22
|
+
sourceMap: HarmonyExportFile;
|
|
23
|
+
transformProfile: 'hermes-stable';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const HermesMagic = Buffer.from('c61fbc03c103191f', 'hex');
|
|
27
|
+
const ManifestSchemaVersion = 1;
|
|
28
|
+
|
|
29
|
+
function exportPaths(plan: {
|
|
30
|
+
exportPaths: {
|
|
31
|
+
bundle: string;
|
|
32
|
+
manifest: string;
|
|
33
|
+
metadataRoot: string;
|
|
34
|
+
rawfileRoot: string;
|
|
35
|
+
sourceMap: string;
|
|
36
|
+
};
|
|
37
|
+
}) {
|
|
38
|
+
return plan.exportPaths;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function assertHermesBundle(file) {
|
|
42
|
+
let handle;
|
|
43
|
+
try {
|
|
44
|
+
handle = await fs.promises.open(file, 'r');
|
|
45
|
+
const header = Buffer.alloc(12);
|
|
46
|
+
const { bytesRead } = await handle.read(header, 0, header.length, 0);
|
|
47
|
+
const stat = await handle.stat();
|
|
48
|
+
|
|
49
|
+
if (bytesRead < header.length || stat.size <= header.length
|
|
50
|
+
|| header.subarray(0, HermesMagic.length).toString('hex') !== HermesMagic.toString('hex')) {
|
|
51
|
+
throw new Error('Expo export did not produce non-empty Hermes bytecode.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { bytecodeVersion: header.readUInt32LE(8), size: stat.size };
|
|
55
|
+
} catch (cause) {
|
|
56
|
+
throw new HarmonyCliError('ERR_HARMONY_EXPORT_INVALID_BUNDLE', 'The Harmony export output is not a valid Hermes bytecode bundle.', { cause, operation: 'validate-export' });
|
|
57
|
+
} finally {
|
|
58
|
+
await handle?.close();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function assertSourceMap(file) {
|
|
63
|
+
try {
|
|
64
|
+
const contents = await fs.promises.readFile(file, 'utf8');
|
|
65
|
+
const sourceMap = JSON.parse(contents);
|
|
66
|
+
const paths = JSON.stringify({ sourceRoot: sourceMap?.sourceRoot, sources: sourceMap?.sources });
|
|
67
|
+
const sourcePaths = [sourceMap?.sourceRoot, ...(sourceMap?.sources || [])]
|
|
68
|
+
.filter(value => typeof value === 'string');
|
|
69
|
+
const hasAbsoluteSource = sourcePaths.some(value => (
|
|
70
|
+
path.posix.isAbsolute(value) || path.win32.isAbsolute(value) || /^[A-Za-z][A-Za-z0-9+.-]*:\/\//u.test(value)
|
|
71
|
+
));
|
|
72
|
+
|
|
73
|
+
if (sourceMap?.version !== 3 || !Array.isArray(sourceMap.sources)
|
|
74
|
+
|| hasAbsoluteSource || paths.includes(os.homedir())) {
|
|
75
|
+
throw new Error('Invalid or host-specific source map.');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return await describeFile(file);
|
|
79
|
+
} catch (cause) {
|
|
80
|
+
throw new HarmonyCliError('ERR_HARMONY_EXPORT_INVALID_SOURCEMAP', 'Expo export did not produce a portable source map.', { cause, operation: 'validate-export' });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function readPreviousManifest(file: string): Promise<HarmonyExportManifest | null> {
|
|
85
|
+
try {
|
|
86
|
+
const manifest = JSON.parse(await fs.promises.readFile(file, 'utf8'));
|
|
87
|
+
if (manifest?.schemaVersion !== ManifestSchemaVersion || !Array.isArray(manifest.assets)) {
|
|
88
|
+
throw new Error('Unsupported export manifest schema.');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const asset of manifest.assets) {
|
|
92
|
+
assertSafeRelative(asset?.path, 'Previous export manifest');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return manifest;
|
|
96
|
+
} catch (cause) {
|
|
97
|
+
if (cause.code === 'ENOENT') return null;
|
|
98
|
+
throw new HarmonyCliError('ERR_HARMONY_EXPORT_MANIFEST', 'Cannot read the previous Harmony export manifest.', { cause, operation: 'publish-export' });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function publishExportAsync(
|
|
103
|
+
projectRoot: string,
|
|
104
|
+
paths: ReturnType<typeof exportPaths>,
|
|
105
|
+
temporary: ExportTemporary,
|
|
106
|
+
entryFile: string,
|
|
107
|
+
bytecode: Awaited<ReturnType<typeof assertHermesBundle>>
|
|
108
|
+
) {
|
|
109
|
+
const previous = await readPreviousManifest(paths.manifest);
|
|
110
|
+
const previouslyOwned = new Set((previous?.assets || []).map(asset => asset.path));
|
|
111
|
+
const assetFiles = await listFiles(temporary.assets);
|
|
112
|
+
const assetOutputs = [];
|
|
113
|
+
|
|
114
|
+
for (const source of assetFiles) {
|
|
115
|
+
const relative = toPosixPath(path.relative(temporary.assets, source));
|
|
116
|
+
const nativeRelative = assertSafeRelative(relative, 'Expo asset output');
|
|
117
|
+
const destination = path.join(paths.rawfileRoot, nativeRelative);
|
|
118
|
+
if (await fs.promises.lstat(destination).catch(error => error.code === 'ENOENT' ? null : Promise.reject(error))) {
|
|
119
|
+
if (!previouslyOwned.has(relative)) {
|
|
120
|
+
throw new HarmonyCliError('ERR_HARMONY_EXPORT_COLLISION', `Expo asset output collides with an unmanaged raw resource: ${relative}.`, { operation: 'publish-export' });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
assetOutputs.push({ descriptor: { path: relative, ...(await describeFile(source)) }, source });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
assetOutputs.sort((left, right) => left.descriptor.path.localeCompare(right.descriptor.path, 'en'));
|
|
128
|
+
const assets = assetOutputs.map(output => output.descriptor);
|
|
129
|
+
|
|
130
|
+
const bundle = await describeFile(temporary.bundle);
|
|
131
|
+
const sourceMap = await describeFile(temporary.sourceMap);
|
|
132
|
+
const manifest: HarmonyExportManifest = {
|
|
133
|
+
assets,
|
|
134
|
+
bundle: {
|
|
135
|
+
bytecodeVersion: bytecode.bytecodeVersion,
|
|
136
|
+
path: toPosixPath(path.relative(projectRoot, paths.bundle)),
|
|
137
|
+
...bundle,
|
|
138
|
+
},
|
|
139
|
+
entryFile: toPosixPath(path.relative(projectRoot, entryFile)),
|
|
140
|
+
platform: 'harmony',
|
|
141
|
+
schemaVersion: ManifestSchemaVersion,
|
|
142
|
+
sourceMap: {
|
|
143
|
+
path: toPosixPath(path.relative(projectRoot, paths.sourceMap)),
|
|
144
|
+
...sourceMap,
|
|
145
|
+
},
|
|
146
|
+
transformProfile: 'hermes-stable',
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
await atomicCopy(temporary.bundle, paths.bundle, paths.rawfileRoot);
|
|
151
|
+
await atomicCopy(temporary.sourceMap, paths.sourceMap, paths.metadataRoot);
|
|
152
|
+
for (const output of assetOutputs) {
|
|
153
|
+
const relative = output.descriptor.path;
|
|
154
|
+
await atomicCopy(
|
|
155
|
+
output.source,
|
|
156
|
+
path.join(paths.rawfileRoot, assertSafeRelative(relative, 'Expo asset output')),
|
|
157
|
+
paths.rawfileRoot
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
await atomicWriteJson(manifest, paths.manifest, path.dirname(paths.manifest));
|
|
161
|
+
|
|
162
|
+
const nextOwned = new Set(assets.map(asset => asset.path));
|
|
163
|
+
for (const stale of previouslyOwned) {
|
|
164
|
+
if (nextOwned.has(stale)) continue;
|
|
165
|
+
const destination = path.join(paths.rawfileRoot, assertSafeRelative(stale, 'Previous export manifest'));
|
|
166
|
+
if (!isInside(paths.rawfileRoot, destination)) continue;
|
|
167
|
+
await fs.promises.rm(destination, { force: true });
|
|
168
|
+
await removeEmptyParents(destination, paths.rawfileRoot);
|
|
169
|
+
}
|
|
170
|
+
} catch (cause) {
|
|
171
|
+
if (cause instanceof HarmonyCliError) {
|
|
172
|
+
throw new HarmonyCliError(cause.code, cause.message, {
|
|
173
|
+
cause,
|
|
174
|
+
exitCode: cause.exitCode,
|
|
175
|
+
operation: cause.operation,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
throw new HarmonyCliError('ERR_HARMONY_EXPORT_PUBLISH', 'Cannot publish the validated Harmony bundle and assets.', { cause, operation: 'publish-export' });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return manifest;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function validatePublishedExportAsync(paths: ReturnType<typeof exportPaths>) {
|
|
186
|
+
const manifest = await readPreviousManifest(paths.manifest);
|
|
187
|
+
if (!manifest) {
|
|
188
|
+
throw new HarmonyCliError('ERR_HARMONY_EXPORT_MANIFEST', 'Harmony export manifest does not exist. Run expo-harmony export:embed first.', { operation: 'check-export' });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const bytecode = await assertHermesBundle(paths.bundle);
|
|
192
|
+
const sourceMap = await assertSourceMap(paths.sourceMap);
|
|
193
|
+
const expectedBundle = await describeFile(paths.bundle);
|
|
194
|
+
if (manifest.bundle?.sha256 !== expectedBundle.sha256
|
|
195
|
+
|| manifest.bundle?.size !== expectedBundle.size
|
|
196
|
+
|| manifest.bundle?.bytecodeVersion !== bytecode.bytecodeVersion
|
|
197
|
+
|| manifest.sourceMap?.sha256 !== sourceMap.sha256
|
|
198
|
+
|| manifest.sourceMap?.size !== sourceMap.size) {
|
|
199
|
+
throw new HarmonyCliError('ERR_HARMONY_EXPORT_MANIFEST', 'Harmony bundle or source map differs from the export manifest.', { operation: 'check-export' });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const asset of manifest.assets) {
|
|
203
|
+
const file = path.join(paths.rawfileRoot, assertSafeRelative(asset.path, 'Export manifest'));
|
|
204
|
+
const actual = await describeFile(file).catch((cause) => {
|
|
205
|
+
throw new HarmonyCliError('ERR_HARMONY_EXPORT_MANIFEST', `Harmony export asset is missing or invalid: ${asset.path}.`, { cause, operation: 'check-export' });
|
|
206
|
+
});
|
|
207
|
+
if (actual.sha256 !== asset.sha256 || actual.size !== asset.size) {
|
|
208
|
+
throw new HarmonyCliError('ERR_HARMONY_EXPORT_MANIFEST', `Harmony export asset differs from the manifest: ${asset.path}.`, { operation: 'check-export' });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return manifest;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export {
|
|
216
|
+
HermesMagic,
|
|
217
|
+
assertHermesBundle,
|
|
218
|
+
assertSourceMap,
|
|
219
|
+
exportPaths,
|
|
220
|
+
publishExportAsync,
|
|
221
|
+
validatePublishedExportAsync,
|
|
222
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { HarmonyCliError } from '../errors';
|
|
2
|
+
import { CommonOptions, parseArgs } from '../args';
|
|
3
|
+
|
|
4
|
+
const ExportEmbedOptions = {
|
|
5
|
+
...CommonOptions,
|
|
6
|
+
'check': { type: 'boolean' },
|
|
7
|
+
'reset-cache': { type: 'boolean' },
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
function parseExportEmbedArgs(argv: string[]) {
|
|
11
|
+
const { positionals, values } = parseArgs(ExportEmbedOptions, argv);
|
|
12
|
+
|
|
13
|
+
if (positionals.length > 1) {
|
|
14
|
+
throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', `Unexpected export:embed positional argument: ${positionals[1]}`, { operation: 'parse-arguments' });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
check: Boolean(values.check),
|
|
19
|
+
help: Boolean(values.help),
|
|
20
|
+
project: positionals[0],
|
|
21
|
+
resetCache: Boolean(values['reset-cache']),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { parseExportEmbedArgs };
|
package/src/file.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import nodeCrypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { isInside } from './path';
|
|
6
|
+
|
|
7
|
+
async function sha256File(file: string): Promise<string> {
|
|
8
|
+
return await new Promise<string>((resolve, reject) => {
|
|
9
|
+
const hash = nodeCrypto.createHash('sha256');
|
|
10
|
+
const input = fs.createReadStream(file);
|
|
11
|
+
|
|
12
|
+
input.on('error', reject);
|
|
13
|
+
input.on('data', (chunk) => {
|
|
14
|
+
const bytes = typeof chunk === 'string' ? new TextEncoder().encode(chunk) : Uint8Array.from(chunk);
|
|
15
|
+
hash.update(bytes);
|
|
16
|
+
});
|
|
17
|
+
input.on('end', () => resolve(hash.digest('hex')));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function describeFile(file: string): Promise<{ sha256: string; size: number }> {
|
|
22
|
+
const stat = await fs.promises.stat(file);
|
|
23
|
+
|
|
24
|
+
if (!stat.isFile()) throw new Error(`Expected a regular file: ${file}`);
|
|
25
|
+
|
|
26
|
+
return { sha256: await sha256File(file), size: stat.size };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function listFiles(root: string): Promise<string[]> {
|
|
30
|
+
const files: string[] = [];
|
|
31
|
+
|
|
32
|
+
async function visit(directory: string): Promise<void> {
|
|
33
|
+
const entries = await fs.promises.readdir(directory, { withFileTypes: true });
|
|
34
|
+
|
|
35
|
+
entries.sort((left, right) => left.name.localeCompare(right.name, 'en'));
|
|
36
|
+
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const file = path.join(directory, entry.name);
|
|
39
|
+
|
|
40
|
+
if (entry.isSymbolicLink()) throw new Error(`Asset output contains a symbolic link: ${entry.name}`);
|
|
41
|
+
if (entry.isDirectory()) await visit(file);
|
|
42
|
+
else if (entry.isFile()) files.push(file);
|
|
43
|
+
else throw new Error(`Asset output contains an unsupported file: ${entry.name}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await visit(root);
|
|
48
|
+
|
|
49
|
+
return files;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function ensureSafeParent(root: string, destination: string): Promise<void> {
|
|
53
|
+
if (!isInside(root, destination) || destination === root) {
|
|
54
|
+
throw new Error('Export destination escapes its owned root.');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await fs.promises.mkdir(root, { recursive: true });
|
|
58
|
+
await fs.promises.mkdir(path.dirname(destination), { recursive: true });
|
|
59
|
+
|
|
60
|
+
const [realRoot, realParent] = await Promise.all([
|
|
61
|
+
fs.promises.realpath(root),
|
|
62
|
+
fs.promises.realpath(path.dirname(destination)),
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
if (!isInside(realRoot, realParent)) {
|
|
66
|
+
throw new Error('Export destination follows a link outside its owned root.');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function atomicCopy(source: string, destination: string, root: string): Promise<void> {
|
|
71
|
+
await ensureSafeParent(root, destination);
|
|
72
|
+
|
|
73
|
+
const temporary = path.join(path.dirname(destination), `.${path.basename(destination)}.expo-${nodeCrypto.randomUUID()}.tmp`);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
await fs.promises.copyFile(source, temporary, fs.constants.COPYFILE_EXCL);
|
|
77
|
+
await fs.promises.rename(temporary, destination);
|
|
78
|
+
} finally {
|
|
79
|
+
await fs.promises.rm(temporary, { force: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function atomicWriteJson(value: unknown, destination: string, root: string): Promise<void> {
|
|
84
|
+
await ensureSafeParent(root, destination);
|
|
85
|
+
|
|
86
|
+
const temporary = path.join(path.dirname(destination), `.${path.basename(destination)}.expo-${nodeCrypto.randomUUID()}.tmp`);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
await fs.promises.writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx' });
|
|
90
|
+
await fs.promises.rename(temporary, destination);
|
|
91
|
+
} finally {
|
|
92
|
+
await fs.promises.rm(temporary, { force: true });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function removeEmptyParents(file: string, stop: string): Promise<void> {
|
|
97
|
+
let directory = path.dirname(file);
|
|
98
|
+
|
|
99
|
+
while (directory !== stop && isInside(stop, directory)) {
|
|
100
|
+
try {
|
|
101
|
+
await fs.promises.rmdir(directory);
|
|
102
|
+
} catch {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
directory = path.dirname(directory);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export { atomicCopy, atomicWriteJson, describeFile, listFiles, removeEmptyParents };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { runAsync } from './cli';
|
|
2
|
+
export { buildHarmonyAsync } from './buildHap/build';
|
|
3
|
+
export type { HarmonyBuildOptions, HarmonyBuildResult } from './buildHap/build';
|
|
4
|
+
export { exportEmbedAsync } from './exportEmbed/export';
|
|
5
|
+
export { resolveHarmonyBuildPlanAsync, resolveHarmonyToolchain } from './tools';
|
|
6
|
+
export { installHarmonyDependenciesAsync } from './run/install';
|
|
7
|
+
export { formatModulesResult, runModulesCommandAsync } from './modules/modules';
|
|
8
|
+
export type { ModulesCommandResult } from './modules/modules';
|
|
9
|
+
export { commitHarmonyNativeBuildCacheAsync, prepareHarmonyNativeBuildCacheAsync } from './run/cache';
|
|
10
|
+
export { runHarmonyAsync } from './run/run';
|
|
11
|
+
export type { HarmonyExportManifest } from './exportEmbed/manifest';
|
|
12
|
+
export type { HarmonyRunOptions, HarmonyRunResult } from './run/run';
|