@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,154 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveModulesAsync,
|
|
3
|
+
searchModulesAsync,
|
|
4
|
+
verifyModulesAsync,
|
|
5
|
+
type BuildType,
|
|
6
|
+
type Diagnostic,
|
|
7
|
+
type ModuleDescriptor,
|
|
8
|
+
type ModuleSource,
|
|
9
|
+
} from '@expo-harmony/expo-modules-autolinking';
|
|
10
|
+
|
|
11
|
+
import { HarmonyCliError } from '../errors';
|
|
12
|
+
import type { ModulesAction } from './options';
|
|
13
|
+
|
|
14
|
+
interface ModulesCommandOptions {
|
|
15
|
+
action: ModulesAction;
|
|
16
|
+
nativeModulesDir?: string;
|
|
17
|
+
packageName?: string;
|
|
18
|
+
variant?: BuildType;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ListedModule {
|
|
22
|
+
packageName: string;
|
|
23
|
+
packageVersion: string;
|
|
24
|
+
source: ModuleSource;
|
|
25
|
+
supportsHarmony: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ModulesListResult {
|
|
29
|
+
action: 'list';
|
|
30
|
+
duplicates: ReadonlyArray<{
|
|
31
|
+
packageName: string;
|
|
32
|
+
revisions: ReadonlyArray<{ path: string; version: string }>;
|
|
33
|
+
}>;
|
|
34
|
+
missingIncludes: ReadonlyArray<string>;
|
|
35
|
+
modules: ReadonlyArray<ListedModule>;
|
|
36
|
+
ok: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ModulesInspectResult {
|
|
40
|
+
action: 'inspect';
|
|
41
|
+
modules: ReadonlyArray<ModuleDescriptor>;
|
|
42
|
+
ok: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface ModulesVerifyResult {
|
|
46
|
+
action: 'verify';
|
|
47
|
+
diagnostics: ReadonlyArray<Diagnostic>;
|
|
48
|
+
modules: ReadonlyArray<ModuleDescriptor>;
|
|
49
|
+
ok: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type ModulesCommandResult = ModulesInspectResult | ModulesListResult | ModulesVerifyResult;
|
|
53
|
+
|
|
54
|
+
function apiOptions(projectRoot: string, options: ModulesCommandOptions) {
|
|
55
|
+
return {
|
|
56
|
+
projectRoot,
|
|
57
|
+
platform: 'harmony' as const,
|
|
58
|
+
...(options.nativeModulesDir === undefined ? {} : { nativeModulesDir: options.nativeModulesDir }),
|
|
59
|
+
...(options.variant === undefined ? {} : { buildType: options.variant }),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function runModulesCommandAsync(
|
|
64
|
+
projectRoot: string,
|
|
65
|
+
options: ModulesCommandOptions
|
|
66
|
+
): Promise<ModulesCommandResult> {
|
|
67
|
+
const shared = apiOptions(projectRoot, options);
|
|
68
|
+
|
|
69
|
+
if (options.action === 'list') {
|
|
70
|
+
const result = await searchModulesAsync(shared);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
action: 'list',
|
|
74
|
+
duplicates: result.duplicates,
|
|
75
|
+
missingIncludes: result.missingIncludes,
|
|
76
|
+
modules: result.modules.map(module => ({
|
|
77
|
+
packageName: module.packageName,
|
|
78
|
+
packageVersion: module.packageVersion,
|
|
79
|
+
source: module.source,
|
|
80
|
+
supportsHarmony: module.supportsHarmony,
|
|
81
|
+
})),
|
|
82
|
+
ok: result.missingIncludes.length === 0,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (options.action === 'inspect') {
|
|
87
|
+
const modules = await resolveModulesAsync(shared);
|
|
88
|
+
const selected = options.packageName === undefined
|
|
89
|
+
? modules
|
|
90
|
+
: modules.filter(module => module.packageName === options.packageName);
|
|
91
|
+
|
|
92
|
+
if (options.packageName !== undefined && selected.length === 0) {
|
|
93
|
+
throw new HarmonyCliError(
|
|
94
|
+
'ERR_HARMONY_MODULE_NOT_FOUND',
|
|
95
|
+
`Harmony module '${options.packageName}' was not discovered. Run 'expo-harmony modules list' to inspect candidates.`,
|
|
96
|
+
{ operation: 'modules-inspect' }
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { action: 'inspect', modules: selected, ok: true };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result = await verifyModulesAsync(shared);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
action: 'verify',
|
|
107
|
+
diagnostics: result.diagnostics,
|
|
108
|
+
modules: result.modules,
|
|
109
|
+
ok: result.valid,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function formatDiagnostic(diagnostic: Diagnostic): string {
|
|
114
|
+
const subject = diagnostic.packageName ? ` ${diagnostic.packageName}` : '';
|
|
115
|
+
return `${diagnostic.severity === 'error' ? '✗' : '!'} [${diagnostic.code}]${subject}: ${diagnostic.message}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatModulesResult(result: ModulesCommandResult): string {
|
|
119
|
+
if (result.action === 'list') {
|
|
120
|
+
const lines = result.modules.map(module => (
|
|
121
|
+
`${module.supportsHarmony ? '✓' : '-'} ${module.packageName}@${module.packageVersion} [${module.source}]`
|
|
122
|
+
));
|
|
123
|
+
for (const packageName of result.missingIncludes) {
|
|
124
|
+
lines.push(`✗ missing required package ${packageName}`);
|
|
125
|
+
}
|
|
126
|
+
for (const duplicate of result.duplicates) {
|
|
127
|
+
lines.push(`! duplicate ${duplicate.packageName}: ${duplicate.revisions.map(item => item.version).join(', ')}`);
|
|
128
|
+
}
|
|
129
|
+
return lines.length === 0 ? 'No native module candidates were discovered.' : lines.join('\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (result.action === 'verify') {
|
|
133
|
+
if (result.diagnostics.length === 0) {
|
|
134
|
+
return `✓ Verified ${result.modules.length} Harmony native module(s).`;
|
|
135
|
+
}
|
|
136
|
+
return result.diagnostics.map(formatDiagnostic).join('\n');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return result.modules.map((module) => {
|
|
140
|
+
const arkTsModules = module.harmony.modules.join(', ') || 'none';
|
|
141
|
+
const har = module.arkTs
|
|
142
|
+
? `${module.arkTs.ohPackageName} <- ${module.arkTs.harPath}`
|
|
143
|
+
: 'none';
|
|
144
|
+
return [
|
|
145
|
+
`${module.packageName}@${module.packageVersion}`,
|
|
146
|
+
` source: ${module.source}`,
|
|
147
|
+
` packageRoot: ${module.packageRoot}`,
|
|
148
|
+
` ArkTS modules: ${arkTsModules}`,
|
|
149
|
+
` HAR: ${har}`,
|
|
150
|
+
].join('\n');
|
|
151
|
+
}).join('\n');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export { formatModulesResult, runModulesCommandAsync };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { CommonOptions, parseArgs } from '../args';
|
|
2
|
+
import { HarmonyCliError } from '../errors';
|
|
3
|
+
|
|
4
|
+
const ModuleOptions = {
|
|
5
|
+
...CommonOptions,
|
|
6
|
+
'package': { short: 'p', type: 'string' },
|
|
7
|
+
'variant': { type: 'string' },
|
|
8
|
+
'native-modules-dir': { type: 'string' },
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export type ModulesAction = 'inspect' | 'list' | 'verify';
|
|
12
|
+
|
|
13
|
+
function parseModulesArgs(argv: string[]) {
|
|
14
|
+
const { positionals, values } = parseArgs(ModuleOptions, argv);
|
|
15
|
+
const action = positionals[0] as ModulesAction | undefined;
|
|
16
|
+
|
|
17
|
+
if (values.help && action === undefined) {
|
|
18
|
+
return {
|
|
19
|
+
action: 'list' as const,
|
|
20
|
+
help: true,
|
|
21
|
+
project: undefined,
|
|
22
|
+
packageName: values.package,
|
|
23
|
+
nativeModulesDir: values['native-modules-dir'],
|
|
24
|
+
variant: values.variant as 'debug' | 'release' | undefined,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!action || !['inspect', 'list', 'verify'].includes(action)) {
|
|
29
|
+
throw new HarmonyCliError(
|
|
30
|
+
'ERR_HARMONY_CONFIG_INVALID',
|
|
31
|
+
'modules requires one of: list, inspect, verify.',
|
|
32
|
+
{ operation: 'parse-arguments' }
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (positionals.length > 2) {
|
|
37
|
+
throw new HarmonyCliError(
|
|
38
|
+
'ERR_HARMONY_CONFIG_INVALID',
|
|
39
|
+
`Unexpected modules positional argument: ${positionals[2]}`,
|
|
40
|
+
{ operation: 'parse-arguments' }
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (values.variant !== undefined && !['debug', 'release'].includes(values.variant)) {
|
|
45
|
+
throw new HarmonyCliError(
|
|
46
|
+
'ERR_HARMONY_CONFIG_INVALID',
|
|
47
|
+
`--variant must be debug or release, received: ${values.variant}`,
|
|
48
|
+
{ operation: 'parse-arguments' }
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (values.package !== undefined && action !== 'inspect') {
|
|
53
|
+
throw new HarmonyCliError(
|
|
54
|
+
'ERR_HARMONY_CONFIG_INVALID',
|
|
55
|
+
'--package is supported by modules inspect only.',
|
|
56
|
+
{ operation: 'parse-arguments' }
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
action,
|
|
62
|
+
help: Boolean(values.help),
|
|
63
|
+
project: positionals[1],
|
|
64
|
+
packageName: values.package,
|
|
65
|
+
nativeModulesDir: values['native-modules-dir'],
|
|
66
|
+
variant: values.variant as 'debug' | 'release' | undefined,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { parseModulesArgs };
|
package/src/path.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
function isInside(root: string, target: string): boolean {
|
|
4
|
+
const relative = path.relative(root, target);
|
|
5
|
+
|
|
6
|
+
return relative === ''
|
|
7
|
+
|| (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function toPosixPath(value: string): string {
|
|
11
|
+
return value.split(path.sep).join('/');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function assertSafeRelative(relative: string, label: string): string {
|
|
15
|
+
const segments = typeof relative === 'string' ? relative.split('/') : [];
|
|
16
|
+
const native = segments.join(path.sep);
|
|
17
|
+
|
|
18
|
+
if (!relative || relative.includes('\\') || relative.includes('\0')
|
|
19
|
+
|| segments.some(segment => !segment || segment === '.' || segment === '..')
|
|
20
|
+
|| path.isAbsolute(native)) {
|
|
21
|
+
throw new Error(`${label} contains an unsafe path.`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return native;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { assertSafeRelative, isInside, toPosixPath };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
compareAsync,
|
|
7
|
+
stageAsync,
|
|
8
|
+
} from '@expo-harmony/prebuild-config/check';
|
|
9
|
+
|
|
10
|
+
import { HarmonyCliError } from '../errors';
|
|
11
|
+
import { isInside } from '../path';
|
|
12
|
+
import { spawnAsync } from '../process';
|
|
13
|
+
import { withHarmonyProjectLockAsync } from '../projectLock';
|
|
14
|
+
import { createHarmonyToolchainEnv, resolveHarmonyBuildPlanAsync, type HarmonyBuildPlan } from '../tools';
|
|
15
|
+
import { resolveExpoCli } from '../expo';
|
|
16
|
+
import { packAsync } from './template';
|
|
17
|
+
|
|
18
|
+
export type { Change as CheckChange } from '@expo-harmony/prebuild-config/check';
|
|
19
|
+
|
|
20
|
+
const IgnoredProjectDirectories = new Set(['.expo', '.git', '.hvigor', '.yarn', 'node_modules']);
|
|
21
|
+
const IgnoredHarmonyGeneratedDirectories = new Set([
|
|
22
|
+
'.cxx',
|
|
23
|
+
'.git',
|
|
24
|
+
'.hvigor',
|
|
25
|
+
'build',
|
|
26
|
+
'node_modules',
|
|
27
|
+
'oh_modules',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
interface CheckOptions {
|
|
31
|
+
buildType?: 'debug' | 'release';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function mirrorRoot(temp, project) {
|
|
35
|
+
const absolute = path.resolve(project);
|
|
36
|
+
const parsed = path.parse(absolute);
|
|
37
|
+
const volume = parsed.root.replace(/[^A-Za-z0-9]+/gu, '') || 'root';
|
|
38
|
+
const segments = absolute.slice(parsed.root.length).split(path.sep).filter(Boolean);
|
|
39
|
+
|
|
40
|
+
return path.join(temp, 'filesystem', volume, ...segments);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function linkModulesAsync(source, target) {
|
|
44
|
+
await fs.promises.mkdir(target, { recursive: true });
|
|
45
|
+
|
|
46
|
+
for (const entry of await fs.promises.readdir(source, { withFileTypes: true })) {
|
|
47
|
+
const from = path.join(source, entry.name);
|
|
48
|
+
const to = path.join(target, entry.name);
|
|
49
|
+
|
|
50
|
+
if (entry.name.startsWith('@') && entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
51
|
+
await fs.promises.mkdir(to);
|
|
52
|
+
for (const child of await fs.promises.readdir(from, { withFileTypes: true })) {
|
|
53
|
+
await fs.promises.symlink(
|
|
54
|
+
path.join(from, child.name),
|
|
55
|
+
path.join(to, child.name),
|
|
56
|
+
process.platform === 'win32' && (child.isDirectory() || child.isSymbolicLink())
|
|
57
|
+
? 'junction'
|
|
58
|
+
: child.isDirectory() ? 'dir' : 'file'
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await fs.promises.symlink(
|
|
65
|
+
from,
|
|
66
|
+
to,
|
|
67
|
+
process.platform === 'win32' && (entry.isDirectory() || entry.isSymbolicLink())
|
|
68
|
+
? 'junction'
|
|
69
|
+
: entry.isDirectory() ? 'dir' : 'file'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isAppLocalHarmonyPath(project: string, source: string): boolean {
|
|
75
|
+
const segments = path.relative(project, source).split(path.sep);
|
|
76
|
+
|
|
77
|
+
return segments.length >= 3
|
|
78
|
+
&& segments[0] === 'modules'
|
|
79
|
+
&& segments[1] !== ''
|
|
80
|
+
&& segments[2] === 'harmony';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function shouldCopyPrebuildCheckPath(
|
|
84
|
+
project: string,
|
|
85
|
+
source: string,
|
|
86
|
+
plan: HarmonyBuildPlan
|
|
87
|
+
): boolean {
|
|
88
|
+
const relative = path.relative(project, source);
|
|
89
|
+
if (!relative) return true;
|
|
90
|
+
|
|
91
|
+
const segments = relative.split(path.sep);
|
|
92
|
+
if (IgnoredProjectDirectories.has(segments[0])) return false;
|
|
93
|
+
|
|
94
|
+
const nativeRoot = isInside(plan.harmonyRoot, source)
|
|
95
|
+
? plan.harmonyRoot
|
|
96
|
+
: isAppLocalHarmonyPath(project, source)
|
|
97
|
+
? path.join(project, ...segments.slice(0, 3))
|
|
98
|
+
: null;
|
|
99
|
+
if (nativeRoot) {
|
|
100
|
+
const native = path.relative(nativeRoot, source).split(path.sep);
|
|
101
|
+
if (native.some(segment => IgnoredHarmonyGeneratedDirectories.has(segment))) return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return source !== plan.exportPaths.bundle;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function copyAsync(
|
|
108
|
+
project: string,
|
|
109
|
+
target: string,
|
|
110
|
+
plan: HarmonyBuildPlan,
|
|
111
|
+
temp = path.dirname(target)
|
|
112
|
+
) {
|
|
113
|
+
await fs.promises.cp(project, target, {
|
|
114
|
+
recursive: true,
|
|
115
|
+
filter: source => shouldCopyPrebuildCheckPath(project, source, plan),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const modules = path.join(project, 'node_modules');
|
|
119
|
+
|
|
120
|
+
if (!fs.existsSync(modules)) {
|
|
121
|
+
throw new HarmonyCliError('ERR_HARMONY_DEPENDENCIES_MISSING', 'node_modules is required for --check.', {
|
|
122
|
+
operation: 'check',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Keep a real node_modules directory in the isolated project and link its
|
|
127
|
+
// entries. Dependency scanners then retain the isolated lexical package path
|
|
128
|
+
// (including scoped packages) instead of collapsing the entire node_modules
|
|
129
|
+
// root to the source project's realpath. The packages remain read-only and
|
|
130
|
+
// are never copied or modified by --check.
|
|
131
|
+
await linkModulesAsync(modules, path.join(target, 'node_modules'));
|
|
132
|
+
await stageAsync(project, target, temp);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function checkUnlockedAsync(project, options: CheckOptions) {
|
|
136
|
+
project = path.resolve(project);
|
|
137
|
+
const plan = await resolveHarmonyBuildPlanAsync(project);
|
|
138
|
+
const temp = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'expo-harmony-check-'));
|
|
139
|
+
const expected = mirrorRoot(temp, project);
|
|
140
|
+
let packed;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
await copyAsync(project, expected, plan, temp);
|
|
144
|
+
packed = await packAsync(project);
|
|
145
|
+
|
|
146
|
+
const expo = resolveExpoCli(project);
|
|
147
|
+
const result = await spawnAsync(process.execPath, [
|
|
148
|
+
expo.cliPath,
|
|
149
|
+
'prebuild',
|
|
150
|
+
expected,
|
|
151
|
+
'--platform', 'harmony',
|
|
152
|
+
'--template', packed.tarball,
|
|
153
|
+
'--no-install',
|
|
154
|
+
], {
|
|
155
|
+
capture: true,
|
|
156
|
+
cwd: expected,
|
|
157
|
+
env: {
|
|
158
|
+
...createHarmonyToolchainEnv(),
|
|
159
|
+
...packed.env,
|
|
160
|
+
...(options.buildType ? { EXPO_HARMONY_BUILD_TYPE: options.buildType } : {}),
|
|
161
|
+
EXPO_HARMONY_CHECK_MIRROR_ROOT: temp,
|
|
162
|
+
},
|
|
163
|
+
operation: 'check-prebuild',
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (result.code !== 0) {
|
|
167
|
+
throw new HarmonyCliError(
|
|
168
|
+
'ERR_HARMONY_MANIFEST_DRIFT',
|
|
169
|
+
`Isolated Expo prebuild failed:\n${result.stderr || result.stdout}`,
|
|
170
|
+
{ exitCode: result.code, operation: 'check-prebuild' }
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return compareAsync(project, expected);
|
|
175
|
+
} finally {
|
|
176
|
+
if (packed) await packed.cleanup();
|
|
177
|
+
await fs.promises.rm(temp, { recursive: true, force: true });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function checkAsync(project, options: CheckOptions = {}) {
|
|
182
|
+
return withHarmonyProjectLockAsync(
|
|
183
|
+
project,
|
|
184
|
+
'prebuild-check',
|
|
185
|
+
() => checkUnlockedAsync(project, options)
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export { checkAsync };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { HarmonyPlatformDirectory } from '@expo-harmony/prebuild-config/build-descriptor';
|
|
5
|
+
|
|
6
|
+
import { HarmonyCliError } from '../errors';
|
|
7
|
+
import { isInside } from '../path';
|
|
8
|
+
import { resolveHarmonyBuildPlanIfPresentAsync } from '../tools';
|
|
9
|
+
|
|
10
|
+
async function assertSafeCleanTarget(projectRoot) {
|
|
11
|
+
const root = await fs.promises.realpath(projectRoot);
|
|
12
|
+
const plan = await resolveHarmonyBuildPlanIfPresentAsync(root);
|
|
13
|
+
const target = plan?.harmonyRoot ?? path.join(root, HarmonyPlatformDirectory);
|
|
14
|
+
let stat;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
stat = await fs.promises.lstat(target);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error.code === 'ENOENT') return target;
|
|
20
|
+
throw new HarmonyCliError(
|
|
21
|
+
error.code || 'ERR_HARMONY_CLEAN_TARGET',
|
|
22
|
+
error.message || `Cannot inspect the Harmony clean target: ${target}. Please delete it manually.`,
|
|
23
|
+
{ cause: error, exitCode: error.exitCode, operation: error.operation }
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!plan) {
|
|
28
|
+
throw new HarmonyCliError(
|
|
29
|
+
'ERR_HARMONY_CLEAN_TARGET',
|
|
30
|
+
`Refusing to clean ${target} because its CNG manifest is missing. Please delete it manually or run prebuild without --clean.`,
|
|
31
|
+
{ operation: 'clean' }
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
36
|
+
throw new HarmonyCliError('ERR_HARMONY_CLEAN_TARGET', `Refusing to clean a non-directory or symlink: ${target}. Please delete it manually.`, {
|
|
37
|
+
operation: 'clean',
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const real = await fs.promises.realpath(target);
|
|
42
|
+
|
|
43
|
+
if (real !== target || !isInside(root, real) || real === root || real === path.parse(real).root) {
|
|
44
|
+
throw new HarmonyCliError('ERR_HARMONY_CLEAN_TARGET', `Unsafe Harmony clean target: ${real}. Please delete it manually.`, { operation: 'clean' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const marker = plan.projectFiles.templateMarker;
|
|
48
|
+
let markerStat;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
markerStat = await fs.promises.lstat(marker);
|
|
52
|
+
} catch (cause) {
|
|
53
|
+
throw new HarmonyCliError(
|
|
54
|
+
'ERR_HARMONY_CLEAN_TARGET',
|
|
55
|
+
`Refusing to clean ${real} because its template marker cannot be inspected. Please delete it manually.`,
|
|
56
|
+
{ cause, operation: 'clean' }
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!isInside(real, marker) || !markerStat.isFile() || markerStat.isSymbolicLink()) {
|
|
61
|
+
throw new HarmonyCliError('ERR_HARMONY_CLEAN_TARGET', `Refusing to clean ${real} because its template marker is unsafe. Please delete it manually.`, { operation: 'clean' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return real;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { assertSafeCleanTarget };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { HarmonyCliError } from '../errors';
|
|
2
|
+
import { CommonOptions, parseArgs } from '../args';
|
|
3
|
+
|
|
4
|
+
const PrebuildOptions = {
|
|
5
|
+
...CommonOptions,
|
|
6
|
+
'bun': { type: 'boolean' },
|
|
7
|
+
'check': { type: 'boolean' },
|
|
8
|
+
'clean': { type: 'boolean' },
|
|
9
|
+
'no-install': { type: 'boolean' },
|
|
10
|
+
'npm': { type: 'boolean' },
|
|
11
|
+
'platform': { short: 'p', type: 'string' },
|
|
12
|
+
'pnpm': { type: 'boolean' },
|
|
13
|
+
'skip-dependency-update': { type: 'string' },
|
|
14
|
+
'template': { short: 't', type: 'string' },
|
|
15
|
+
'yarn': { type: 'boolean' },
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
function parsePrebuildArgs(argv: string[], options: { allowProject?: boolean } = {}) {
|
|
19
|
+
const { positionals, values } = parseArgs(PrebuildOptions, argv);
|
|
20
|
+
|
|
21
|
+
if (values.platform !== undefined) {
|
|
22
|
+
throw new HarmonyCliError(
|
|
23
|
+
'ERR_HARMONY_CONFIG_INVALID',
|
|
24
|
+
'--platform is fixed to harmony by expo-harmony.',
|
|
25
|
+
{ operation: 'parse-arguments' }
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (values.template !== undefined) {
|
|
30
|
+
throw new HarmonyCliError(
|
|
31
|
+
'ERR_HARMONY_CONFIG_INVALID',
|
|
32
|
+
'--template is managed by expo-harmony.',
|
|
33
|
+
{ operation: 'parse-arguments' }
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (positionals.length > (options.allowProject ? 1 : 0)) {
|
|
38
|
+
const unexpected = positionals[options.allowProject ? 1 : 0];
|
|
39
|
+
throw new HarmonyCliError(
|
|
40
|
+
'ERR_HARMONY_CONFIG_INVALID',
|
|
41
|
+
`Unexpected prebuild positional argument: ${unexpected}`,
|
|
42
|
+
{ operation: 'parse-arguments' }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const packageManagerFlags = ['npm', 'yarn', 'pnpm', 'bun'].filter(name => values[name]);
|
|
47
|
+
if (packageManagerFlags.length > 1) {
|
|
48
|
+
throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', 'Choose at most one package manager: --npm, --yarn, --pnpm, or --bun.', { operation: 'parse-arguments' });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const passthrough: string[] = [];
|
|
52
|
+
for (const name of ['clean', 'npm', 'yarn', 'pnpm', 'bun', 'no-install']) {
|
|
53
|
+
if (values[name]) passthrough.push(`--${name}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (values['skip-dependency-update'] !== undefined) {
|
|
57
|
+
passthrough.push('--skip-dependency-update', values['skip-dependency-update']);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
check: Boolean(values.check),
|
|
62
|
+
clean: Boolean(values.clean),
|
|
63
|
+
help: Boolean(values.help),
|
|
64
|
+
passthrough,
|
|
65
|
+
project: positionals[0],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { parsePrebuildArgs };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import { HarmonyCliError } from '../errors';
|
|
4
|
+
import { formatDiagnostics, spawnAsync } from '../process';
|
|
5
|
+
import { withHarmonyProjectLockAsync } from '../projectLock';
|
|
6
|
+
import { resolveExpoCli } from '../expo';
|
|
7
|
+
import { createHarmonyToolchainEnv, resolveHarmonyBuildPlanAsync } from '../tools';
|
|
8
|
+
import { assertSafeCleanTarget } from './clean';
|
|
9
|
+
import { packAsync } from './template';
|
|
10
|
+
|
|
11
|
+
interface PrebuildOptions {
|
|
12
|
+
buildType?: 'debug' | 'release';
|
|
13
|
+
capture?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function prebuildParsedUnlockedAsync(
|
|
17
|
+
projectRoot: string,
|
|
18
|
+
passthrough: string[],
|
|
19
|
+
options: PrebuildOptions = {}
|
|
20
|
+
) {
|
|
21
|
+
if (passthrough.includes('--clean')) await assertSafeCleanTarget(projectRoot);
|
|
22
|
+
|
|
23
|
+
const expo = resolveExpoCli(projectRoot);
|
|
24
|
+
const packed = await packAsync(projectRoot);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const result = await spawnAsync(process.execPath, [
|
|
28
|
+
expo.cliPath,
|
|
29
|
+
'prebuild', projectRoot,
|
|
30
|
+
'--platform', 'harmony',
|
|
31
|
+
'--template', packed.tarball,
|
|
32
|
+
...passthrough,
|
|
33
|
+
], {
|
|
34
|
+
capture: Boolean(options.capture),
|
|
35
|
+
cwd: projectRoot,
|
|
36
|
+
env: {
|
|
37
|
+
...createHarmonyToolchainEnv(),
|
|
38
|
+
...packed.env,
|
|
39
|
+
...(options.buildType ? { EXPO_HARMONY_BUILD_TYPE: options.buildType } : {}),
|
|
40
|
+
},
|
|
41
|
+
operation: 'expo-prebuild',
|
|
42
|
+
outputLimit: 4 * 1024 * 1024,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (result.code !== 0) {
|
|
46
|
+
const diagnostics = options.capture ? formatDiagnostics(result) : '';
|
|
47
|
+
throw new HarmonyCliError('ERR_HARMONY_PREBUILD_FAILED', `Expo prebuild exited with code ${result.code}.${diagnostics ? `\n${diagnostics}` : ''}`, {
|
|
48
|
+
exitCode: result.code,
|
|
49
|
+
operation: 'expo-prebuild',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const plan = await resolveHarmonyBuildPlanAsync(projectRoot, {
|
|
55
|
+
buildMode: options.buildType,
|
|
56
|
+
});
|
|
57
|
+
await fs.promises.access(plan.projectFiles.templateMarker, fs.constants.R_OK);
|
|
58
|
+
} catch (cause) {
|
|
59
|
+
throw new HarmonyCliError('ERR_HARMONY_TEMPLATE_INVALID', 'Expo prebuild exited successfully but the Harmony marker or CNG manifest is invalid.', { cause, operation: 'verify-prebuild' });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
} finally {
|
|
64
|
+
await packed.cleanup();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function prebuildParsedAsync(
|
|
69
|
+
projectRoot: string,
|
|
70
|
+
passthrough: string[],
|
|
71
|
+
options: PrebuildOptions = {}
|
|
72
|
+
) {
|
|
73
|
+
return withHarmonyProjectLockAsync(
|
|
74
|
+
projectRoot,
|
|
75
|
+
'prebuild',
|
|
76
|
+
() => prebuildParsedUnlockedAsync(projectRoot, passthrough, options)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export { prebuildParsedAsync };
|