@expo-harmony/prebuild-config 55.0.0-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 +68 -0
- package/build/index.js +26 -0
- package/package.json +87 -0
- package/src/api/signing.ts +4 -0
- package/src/buildDescriptor.ts +277 -0
- package/src/check.ts +390 -0
- package/src/dependencies.ts +123 -0
- package/src/errors.ts +23 -0
- package/src/generatedFiles.ts +39 -0
- package/src/index.ts +25 -0
- package/src/manifest.ts +303 -0
- package/src/mods/withAutolinkingMods.ts +92 -0
- package/src/mods/withEntryMods.ts +332 -0
- package/src/mods/withPreparationMod.ts +85 -0
- package/src/mods/withProjectMods.ts +258 -0
- package/src/mods/withSourceMods.ts +32 -0
- package/src/native.ts +10 -0
- package/src/packageMetadata.ts +8 -0
- package/src/plugin.ts +7 -0
- package/src/reconcile.ts +70 -0
- package/src/renderers.ts +90 -0
- package/src/signing.ts +251 -0
- package/src/stale.ts +103 -0
- package/src/template.ts +172 -0
- package/src/withHarmonyPrebuildConfig.ts +47 -0
- package/tsconfig.json +20 -0
package/src/manifest.ts
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { normalizeHarmonyConfigPlugins, stableHarmonyJson } from '@expo-harmony/config-plugins';
|
|
6
|
+
import type {
|
|
7
|
+
HarmonyConfigPluginOwnership,
|
|
8
|
+
NormalizedHarmonyConfig,
|
|
9
|
+
} from '@expo-harmony/config-plugins';
|
|
10
|
+
|
|
11
|
+
import { HarmonyPrebuildError } from './errors';
|
|
12
|
+
import {
|
|
13
|
+
createHarmonyBuildDescriptor,
|
|
14
|
+
validateHarmonyBuildDescriptor,
|
|
15
|
+
type HarmonyBuildDescriptor,
|
|
16
|
+
} from './buildDescriptor';
|
|
17
|
+
import { PackageMetadata } from './packageMetadata';
|
|
18
|
+
|
|
19
|
+
const GeneratorVersion = PackageMetadata.version;
|
|
20
|
+
const ManifestSchemaVersion = 2;
|
|
21
|
+
const CngManifestPath = '.expo/harmony/cng-manifest.json';
|
|
22
|
+
const Sha256Pattern = /^[a-f0-9]{64}$/u;
|
|
23
|
+
|
|
24
|
+
interface ManagedFile {
|
|
25
|
+
owner: string;
|
|
26
|
+
path: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface CngManifest {
|
|
30
|
+
build: HarmonyBuildDescriptor;
|
|
31
|
+
configPlugins: HarmonyConfigPluginOwnership[];
|
|
32
|
+
generatedAt: null;
|
|
33
|
+
generator: {
|
|
34
|
+
package: '@expo-harmony/prebuild-config';
|
|
35
|
+
version: string;
|
|
36
|
+
};
|
|
37
|
+
inputs: {
|
|
38
|
+
autolinkingHash: string;
|
|
39
|
+
configHash: string;
|
|
40
|
+
};
|
|
41
|
+
managedFiles: Array<ManagedFile & { sha256: string }>;
|
|
42
|
+
managedIdentity: {
|
|
43
|
+
abilityName: string;
|
|
44
|
+
moduleName: string;
|
|
45
|
+
productName: string;
|
|
46
|
+
targetName: string;
|
|
47
|
+
};
|
|
48
|
+
modules: Array<{ packageName: string; packageVersion: string }>;
|
|
49
|
+
schemaVersion: 2;
|
|
50
|
+
signingConfigName: string | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
54
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
58
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ValidateCngManifestOptions {
|
|
62
|
+
file?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validateCngManifest(manifest: unknown, options: ValidateCngManifestOptions = {}): CngManifest {
|
|
66
|
+
const file = options.file;
|
|
67
|
+
|
|
68
|
+
if (!isRecord(manifest)) {
|
|
69
|
+
throw new HarmonyPrebuildError(
|
|
70
|
+
'ERR_HARMONY_MANIFEST_INVALID',
|
|
71
|
+
'Harmony CNG manifest must be an object.',
|
|
72
|
+
{ file, operation: 'validate-manifest' }
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (manifest.generatedAt !== null) {
|
|
76
|
+
throw new HarmonyPrebuildError(
|
|
77
|
+
'ERR_HARMONY_MANIFEST_INVALID',
|
|
78
|
+
'Harmony CNG manifest generatedAt must be the deterministic null sentinel.',
|
|
79
|
+
{ file, operation: 'validate-manifest' }
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (manifest.schemaVersion !== ManifestSchemaVersion) {
|
|
83
|
+
throw new HarmonyPrebuildError(
|
|
84
|
+
'ERR_HARMONY_MANIFEST_INVALID',
|
|
85
|
+
`Unsupported Harmony CNG manifest schema: ${manifest.schemaVersion}.`,
|
|
86
|
+
{ file, operation: 'validate-manifest' }
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (!isRecord(manifest.generator)
|
|
90
|
+
|| manifest.generator.package !== '@expo-harmony/prebuild-config'
|
|
91
|
+
|| !isNonEmptyString(manifest.generator.version)) {
|
|
92
|
+
throw new HarmonyPrebuildError(
|
|
93
|
+
'ERR_HARMONY_MANIFEST_INVALID',
|
|
94
|
+
'Harmony CNG manifest has an invalid generator.',
|
|
95
|
+
{ file, operation: 'validate-manifest' }
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let build: HarmonyBuildDescriptor;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
build = validateHarmonyBuildDescriptor(manifest.build);
|
|
103
|
+
manifest.build = build;
|
|
104
|
+
} catch (cause) {
|
|
105
|
+
throw new HarmonyPrebuildError(
|
|
106
|
+
'ERR_HARMONY_MANIFEST_INVALID',
|
|
107
|
+
`Harmony CNG manifest has an invalid build descriptor: ${cause.message}`,
|
|
108
|
+
{ cause, file, operation: 'validate-manifest' }
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!isRecord(manifest.inputs)
|
|
113
|
+
|| typeof manifest.inputs.autolinkingHash !== 'string'
|
|
114
|
+
|| typeof manifest.inputs.configHash !== 'string'
|
|
115
|
+
|| !Sha256Pattern.test(manifest.inputs.autolinkingHash)
|
|
116
|
+
|| !Sha256Pattern.test(manifest.inputs.configHash)) {
|
|
117
|
+
throw new HarmonyPrebuildError(
|
|
118
|
+
'ERR_HARMONY_MANIFEST_INVALID',
|
|
119
|
+
'Harmony CNG manifest has invalid input hashes.',
|
|
120
|
+
{ file, operation: 'validate-manifest' }
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (!Array.isArray(manifest.managedFiles)) {
|
|
124
|
+
throw new HarmonyPrebuildError(
|
|
125
|
+
'ERR_HARMONY_MANIFEST_INVALID',
|
|
126
|
+
'Harmony CNG manifest managedFiles must be an array.',
|
|
127
|
+
{ file, operation: 'validate-manifest' }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
manifest.configPlugins = normalizeHarmonyConfigPlugins(manifest.configPlugins);
|
|
133
|
+
} catch (cause) {
|
|
134
|
+
throw new HarmonyPrebuildError(
|
|
135
|
+
'ERR_HARMONY_MANIFEST_INVALID',
|
|
136
|
+
`Harmony CNG manifest has invalid config-plugin ownership: ${cause.message}`,
|
|
137
|
+
{ cause, file, operation: 'validate-manifest' }
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const paths = new Set();
|
|
142
|
+
|
|
143
|
+
for (const descriptor of manifest.managedFiles) {
|
|
144
|
+
const relative = descriptor?.path;
|
|
145
|
+
const segments = typeof relative === 'string' ? relative.split('/') : [];
|
|
146
|
+
|
|
147
|
+
if (!isRecord(descriptor)
|
|
148
|
+
|| !isNonEmptyString(descriptor.owner)
|
|
149
|
+
|| !isNonEmptyString(relative)
|
|
150
|
+
|| relative.includes('\\')
|
|
151
|
+
|| path.posix.isAbsolute(relative)
|
|
152
|
+
|| path.win32.isAbsolute(relative)
|
|
153
|
+
|| path.posix.normalize(relative) !== relative
|
|
154
|
+
|| segments.some(segment => !segment || segment === '.' || segment === '..')
|
|
155
|
+
|| typeof descriptor.sha256 !== 'string'
|
|
156
|
+
|| !Sha256Pattern.test(descriptor.sha256)) {
|
|
157
|
+
throw new HarmonyPrebuildError(
|
|
158
|
+
'ERR_HARMONY_MANIFEST_INVALID',
|
|
159
|
+
`Harmony CNG manifest contains an invalid managed file: ${relative}.`,
|
|
160
|
+
{ file, operation: 'validate-manifest' }
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (paths.has(relative)) {
|
|
164
|
+
throw new HarmonyPrebuildError(
|
|
165
|
+
'ERR_HARMONY_MANIFEST_INVALID',
|
|
166
|
+
`Harmony CNG manifest contains a duplicate managed path: ${relative}.`,
|
|
167
|
+
{ file, operation: 'validate-manifest' }
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
paths.add(relative);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!Array.isArray(manifest.modules)) {
|
|
175
|
+
throw new HarmonyPrebuildError(
|
|
176
|
+
'ERR_HARMONY_MANIFEST_INVALID',
|
|
177
|
+
'Harmony CNG manifest modules must be an array.',
|
|
178
|
+
{ file, operation: 'validate-manifest' }
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const names = new Set();
|
|
183
|
+
|
|
184
|
+
for (const module of manifest.modules) {
|
|
185
|
+
if (!isRecord(module)
|
|
186
|
+
|| !isNonEmptyString(module.packageName)
|
|
187
|
+
|| !isNonEmptyString(module.packageVersion)) {
|
|
188
|
+
throw new HarmonyPrebuildError(
|
|
189
|
+
'ERR_HARMONY_MANIFEST_INVALID',
|
|
190
|
+
'Harmony CNG manifest contains an invalid module descriptor.',
|
|
191
|
+
{ file, operation: 'validate-manifest' }
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (names.has(module.packageName)) {
|
|
195
|
+
throw new HarmonyPrebuildError(
|
|
196
|
+
'ERR_HARMONY_MANIFEST_INVALID',
|
|
197
|
+
`Harmony CNG manifest contains a duplicate module: ${module.packageName}.`,
|
|
198
|
+
{ file, operation: 'validate-manifest' }
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
names.add(module.packageName);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!isRecord(manifest.managedIdentity)
|
|
206
|
+
|| ['abilityName', 'moduleName', 'productName', 'targetName']
|
|
207
|
+
.some(field => !isNonEmptyString(manifest.managedIdentity[field]))) {
|
|
208
|
+
throw new HarmonyPrebuildError(
|
|
209
|
+
'ERR_HARMONY_MANIFEST_INVALID',
|
|
210
|
+
'Harmony CNG manifest has an invalid managed identity.',
|
|
211
|
+
{ file, operation: 'validate-manifest' }
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (build.identity.abilityName !== manifest.managedIdentity.abilityName
|
|
215
|
+
|| build.identity.moduleName !== manifest.managedIdentity.moduleName
|
|
216
|
+
|| build.identity.productName !== manifest.managedIdentity.productName
|
|
217
|
+
|| build.identity.targetName !== manifest.managedIdentity.targetName) {
|
|
218
|
+
throw new HarmonyPrebuildError(
|
|
219
|
+
'ERR_HARMONY_MANIFEST_INVALID',
|
|
220
|
+
'Harmony CNG manifest build identity does not match its managed identity.',
|
|
221
|
+
{ file, operation: 'validate-manifest' }
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (manifest.signingConfigName !== null
|
|
225
|
+
&& !isNonEmptyString(manifest.signingConfigName)) {
|
|
226
|
+
throw new HarmonyPrebuildError(
|
|
227
|
+
'ERR_HARMONY_MANIFEST_INVALID',
|
|
228
|
+
'Harmony CNG manifest has an invalid signing config name.',
|
|
229
|
+
{ file, operation: 'validate-manifest' }
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return manifest as unknown as CngManifest;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function hashSha256(value: string | Uint8Array) {
|
|
237
|
+
return createHash('sha256').update(value).digest('hex');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function hashFile(file: string) {
|
|
241
|
+
return hashSha256(Uint8Array.from(await fs.readFile(file)));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function createCngManifest(
|
|
245
|
+
root: string,
|
|
246
|
+
config: NormalizedHarmonyConfig,
|
|
247
|
+
managed: readonly ManagedFile[],
|
|
248
|
+
modules: ReadonlyArray<{ packageName: string; packageVersion: string }> = [],
|
|
249
|
+
signing: string | null = null,
|
|
250
|
+
plugins: readonly HarmonyConfigPluginOwnership[] = []
|
|
251
|
+
): Promise<CngManifest> {
|
|
252
|
+
const build = createHarmonyBuildDescriptor(config, signing);
|
|
253
|
+
const files = [];
|
|
254
|
+
|
|
255
|
+
for (const item of managed) {
|
|
256
|
+
const target = path.join(root, ...item.path.split('/'));
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
files.push({ owner: item.owner, path: item.path, sha256: await hashFile(target) });
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (error.code !== 'ENOENT') return Promise.reject(error);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
files.sort((left, right) => left.path.localeCompare(right.path, 'en'));
|
|
266
|
+
|
|
267
|
+
const autolinkingFile = path.join(root, '.expo/harmony/autolinking.json');
|
|
268
|
+
const autolinkingHash = await hashFile(autolinkingFile)
|
|
269
|
+
.catch(error => error.code === 'ENOENT' ? hashSha256('') : Promise.reject(error));
|
|
270
|
+
|
|
271
|
+
return validateCngManifest({
|
|
272
|
+
build,
|
|
273
|
+
generatedAt: null,
|
|
274
|
+
configPlugins: normalizeHarmonyConfigPlugins(plugins),
|
|
275
|
+
generator: { package: '@expo-harmony/prebuild-config', version: GeneratorVersion },
|
|
276
|
+
inputs: {
|
|
277
|
+
autolinkingHash,
|
|
278
|
+
configHash: hashSha256(stableHarmonyJson(config)),
|
|
279
|
+
},
|
|
280
|
+
managedFiles: files,
|
|
281
|
+
managedIdentity: {
|
|
282
|
+
abilityName: build.identity.abilityName,
|
|
283
|
+
moduleName: build.identity.moduleName,
|
|
284
|
+
productName: build.identity.productName,
|
|
285
|
+
targetName: build.identity.targetName,
|
|
286
|
+
},
|
|
287
|
+
modules: modules.map(module => ({ packageName: module.packageName, packageVersion: module.packageVersion })),
|
|
288
|
+
schemaVersion: ManifestSchemaVersion,
|
|
289
|
+
signingConfigName: signing,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export {
|
|
294
|
+
CngManifestPath,
|
|
295
|
+
ManifestSchemaVersion,
|
|
296
|
+
createCngManifest,
|
|
297
|
+
hashFile,
|
|
298
|
+
hashSha256,
|
|
299
|
+
validateCngManifest,
|
|
300
|
+
};
|
|
301
|
+
export type {
|
|
302
|
+
CngManifest,
|
|
303
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { linkModulesAsync } from '@expo-harmony/expo-modules-autolinking';
|
|
5
|
+
import {
|
|
6
|
+
getHarmonyConfigPlugins,
|
|
7
|
+
recordManagedFile,
|
|
8
|
+
stableHarmonyJson,
|
|
9
|
+
withCngManifest,
|
|
10
|
+
withHarmonyAutolinking,
|
|
11
|
+
} from '@expo-harmony/config-plugins';
|
|
12
|
+
|
|
13
|
+
import { HarmonyPrebuildError } from '../errors';
|
|
14
|
+
import {
|
|
15
|
+
createHarmonyBuildDescriptor,
|
|
16
|
+
resolveHarmonyBuildPath,
|
|
17
|
+
} from '../buildDescriptor';
|
|
18
|
+
import { CngManifestPath, createCngManifest } from '../manifest';
|
|
19
|
+
|
|
20
|
+
function formatAutolinkingDiagnostics(cause) {
|
|
21
|
+
return Array.isArray(cause.diagnostics)
|
|
22
|
+
? ` ${cause.diagnostics.map(item => `[${item.code}] ${item.message}`).join(' ')}`
|
|
23
|
+
: '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function withAutolinkingMods(config, harmony, options) {
|
|
27
|
+
config = withHarmonyAutolinking(config, async (mod) => {
|
|
28
|
+
try {
|
|
29
|
+
const root = mod.modRequest.projectRoot;
|
|
30
|
+
const project = await fs.promises.realpath(root);
|
|
31
|
+
|
|
32
|
+
const build = createHarmonyBuildDescriptor(harmony, mod._internal?.harmonySigningConfig?.name ?? null);
|
|
33
|
+
const platform = resolveHarmonyBuildPath(project, build.harmonyRoot);
|
|
34
|
+
const mode = (options.buildType || process.env.EXPO_HARMONY_BUILD_TYPE || 'debug') as 'debug' | 'release';
|
|
35
|
+
|
|
36
|
+
const result = await linkModulesAsync({
|
|
37
|
+
projectRoot: project,
|
|
38
|
+
harmonyProjectPath: platform,
|
|
39
|
+
buildType: mode,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
mod._internal ??= {};
|
|
43
|
+
mod._internal.harmonyAutolinkingModules = result.modules;
|
|
44
|
+
|
|
45
|
+
for (const relative of result.managedArtifacts) {
|
|
46
|
+
const target = path.join(root, ...relative.split('/'));
|
|
47
|
+
|
|
48
|
+
if (fs.existsSync(target)) {
|
|
49
|
+
recordManagedFile(mod, target, 'autolinking');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
recordManagedFile(
|
|
54
|
+
mod,
|
|
55
|
+
resolveHarmonyBuildPath(root, build.nativeInputs.manifest),
|
|
56
|
+
'autolinking'
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return mod;
|
|
60
|
+
} catch (cause) {
|
|
61
|
+
throw new HarmonyPrebuildError(
|
|
62
|
+
'ERR_HARMONY_AUTOLINK_FAILED',
|
|
63
|
+
`Harmony autolinking failed: ${cause.message}${formatAutolinkingDiagnostics(cause)}`,
|
|
64
|
+
{ cause, operation: 'autolinking' }
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
config = withCngManifest(config, async (mod) => {
|
|
70
|
+
const managed = mod._internal?.harmonyManagedFiles || [];
|
|
71
|
+
|
|
72
|
+
const manifest = await createCngManifest(
|
|
73
|
+
mod.modRequest.projectRoot,
|
|
74
|
+
harmony,
|
|
75
|
+
managed,
|
|
76
|
+
mod._internal?.harmonyAutolinkingModules || [],
|
|
77
|
+
mod._internal?.harmonySigningConfig?.name ?? null,
|
|
78
|
+
getHarmonyConfigPlugins(mod)
|
|
79
|
+
);
|
|
80
|
+
const file = resolveHarmonyBuildPath(mod.modRequest.projectRoot, CngManifestPath);
|
|
81
|
+
|
|
82
|
+
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
|
83
|
+
await fs.promises.writeFile(file, stableHarmonyJson(manifest));
|
|
84
|
+
|
|
85
|
+
mod._internal ??= {};
|
|
86
|
+
mod._internal.harmonyCngManifest = manifest;
|
|
87
|
+
|
|
88
|
+
return mod;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return config;
|
|
92
|
+
}
|