@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/check.ts
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
+
|
|
5
|
+
import { getConfig } from '@expo/config';
|
|
6
|
+
import { normalizeHarmonyConfig, stableHarmonyJson } from '@expo-harmony/config-plugins';
|
|
7
|
+
import { canonicalizeAutolinkingArtifacts } from '@expo-harmony/expo-modules-autolinking';
|
|
8
|
+
import JSON5 from 'json5';
|
|
9
|
+
|
|
10
|
+
import { HarmonyPrebuildError } from './errors';
|
|
11
|
+
import {
|
|
12
|
+
createHarmonyBuildDescriptor,
|
|
13
|
+
resolveHarmonyBuildPath,
|
|
14
|
+
} from './buildDescriptor';
|
|
15
|
+
import {
|
|
16
|
+
CngManifestPath,
|
|
17
|
+
hashSha256,
|
|
18
|
+
validateCngManifest,
|
|
19
|
+
type CngManifest,
|
|
20
|
+
} from './manifest';
|
|
21
|
+
import { validateHarmonySigningConfigFile } from './signing';
|
|
22
|
+
|
|
23
|
+
interface Change {
|
|
24
|
+
path: string;
|
|
25
|
+
type: 'added' | 'changed' | 'deleted';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Result {
|
|
29
|
+
changes: Change[];
|
|
30
|
+
clean: boolean;
|
|
31
|
+
expected: CngManifest;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isInside(root: string, target: string): boolean {
|
|
35
|
+
const relative = path.relative(root, target);
|
|
36
|
+
return relative === ''
|
|
37
|
+
|| (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function mirrorPath(temp: string, source: string): string {
|
|
41
|
+
const absolute = path.resolve(source);
|
|
42
|
+
const parsed = path.parse(absolute);
|
|
43
|
+
const volume = parsed.root.replace(/[^A-Za-z0-9]+/gu, '') || 'root';
|
|
44
|
+
const segments = absolute.slice(parsed.root.length).split(path.sep).filter(Boolean);
|
|
45
|
+
|
|
46
|
+
return path.join(temp, 'filesystem', volume, ...segments);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function stageAutolinkingAsync(project: string, target: string): Promise<void> {
|
|
50
|
+
const source = path.join(project, '.expo/harmony/autolinking.json');
|
|
51
|
+
|
|
52
|
+
let stat;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
stat = await fs.promises.lstat(source);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error.code === 'ENOENT') return;
|
|
58
|
+
throw new HarmonyPrebuildError(
|
|
59
|
+
error.code || 'ERR_HARMONY_MANIFEST_DRIFT',
|
|
60
|
+
error.message || `Cannot inspect the previous autolinking manifest: ${source}`,
|
|
61
|
+
{ cause: error, operation: error.operation || 'check' }
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return;
|
|
66
|
+
|
|
67
|
+
const content = await fs.promises.readFile(source, 'utf8');
|
|
68
|
+
const file = path.join(target, '.expo/harmony/autolinking.json');
|
|
69
|
+
|
|
70
|
+
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
|
71
|
+
await fs.promises.writeFile(file, content);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function matchesIdentity(
|
|
75
|
+
project: string,
|
|
76
|
+
cng: CngManifest
|
|
77
|
+
): Promise<boolean> {
|
|
78
|
+
const identity = cng.managedIdentity;
|
|
79
|
+
let profile;
|
|
80
|
+
let module;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const [profileSource, moduleSource] = await Promise.all([
|
|
84
|
+
fs.promises.readFile(resolveHarmonyBuildPath(
|
|
85
|
+
project,
|
|
86
|
+
cng.build.projectFiles.projectBuildProfile
|
|
87
|
+
), 'utf8'),
|
|
88
|
+
fs.promises.readFile(resolveHarmonyBuildPath(
|
|
89
|
+
project,
|
|
90
|
+
cng.build.projectFiles.moduleJson
|
|
91
|
+
), 'utf8'),
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
profile = JSON5.parse(profileSource);
|
|
95
|
+
module = JSON5.parse(moduleSource);
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const moduleProfile = Array.isArray(profile?.modules)
|
|
101
|
+
? profile.modules.find(item => item?.name === identity.moduleName)
|
|
102
|
+
: null;
|
|
103
|
+
const target = Array.isArray(moduleProfile?.targets)
|
|
104
|
+
? moduleProfile.targets.find(item => item?.name === identity.targetName)
|
|
105
|
+
: null;
|
|
106
|
+
const products = Array.isArray(profile?.app?.products) ? profile.app.products : [];
|
|
107
|
+
const abilities = Array.isArray(module?.module?.abilities) ? module.module.abilities : [];
|
|
108
|
+
|
|
109
|
+
return module?.module?.name === identity.moduleName
|
|
110
|
+
&& module?.module?.mainElement === identity.abilityName
|
|
111
|
+
&& abilities.some(item => item?.name === identity.abilityName)
|
|
112
|
+
&& products.some(item => item?.name === identity.productName)
|
|
113
|
+
&& Array.isArray(target?.applyToProducts)
|
|
114
|
+
&& target.applyToProducts.includes(identity.productName);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function readManifestIfPresentAsync(project: string): Promise<CngManifest | null> {
|
|
118
|
+
const file = resolveHarmonyBuildPath(project, CngManifestPath);
|
|
119
|
+
|
|
120
|
+
let source;
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
source = await fs.promises.readFile(file, 'utf8');
|
|
124
|
+
} catch (cause) {
|
|
125
|
+
if (cause.code === 'ENOENT') return null;
|
|
126
|
+
|
|
127
|
+
throw new HarmonyPrebuildError(
|
|
128
|
+
'ERR_HARMONY_MANIFEST_DRIFT',
|
|
129
|
+
`Cannot read CNG manifest: ${file}`,
|
|
130
|
+
{ cause, file, operation: 'check' }
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
return validateCngManifest(JSON.parse(source), { file });
|
|
136
|
+
} catch (cause) {
|
|
137
|
+
throw new HarmonyPrebuildError(
|
|
138
|
+
'ERR_HARMONY_MANIFEST_DRIFT',
|
|
139
|
+
`Cannot read CNG manifest: ${file}`,
|
|
140
|
+
{ cause, file, operation: 'check' }
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function readManifestAsync(project: string): Promise<CngManifest> {
|
|
146
|
+
const manifest = await readManifestIfPresentAsync(project);
|
|
147
|
+
|
|
148
|
+
if (!manifest) {
|
|
149
|
+
const file = resolveHarmonyBuildPath(project, CngManifestPath);
|
|
150
|
+
throw new HarmonyPrebuildError(
|
|
151
|
+
'ERR_HARMONY_MANIFEST_DRIFT',
|
|
152
|
+
`Cannot read CNG manifest: ${file}`,
|
|
153
|
+
{ file, operation: 'check' }
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return manifest;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function stageManifestAsync(project: string, target: string): Promise<void> {
|
|
161
|
+
const source = resolveHarmonyBuildPath(project, CngManifestPath);
|
|
162
|
+
|
|
163
|
+
let stat;
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
stat = await fs.promises.lstat(source);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error.code === 'ENOENT') return;
|
|
169
|
+
throw new HarmonyPrebuildError(
|
|
170
|
+
error.code || 'ERR_HARMONY_MANIFEST_DRIFT',
|
|
171
|
+
error.message || `Cannot inspect the previous CNG manifest: ${source}`,
|
|
172
|
+
{ cause: error, operation: error.operation || 'check' }
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!stat.isFile() || stat.isSymbolicLink()) return;
|
|
177
|
+
|
|
178
|
+
let cng;
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
cng = validateCngManifest(JSON.parse(await fs.promises.readFile(source, 'utf8')), {
|
|
182
|
+
file: source,
|
|
183
|
+
});
|
|
184
|
+
} catch (cause) {
|
|
185
|
+
throw new HarmonyPrebuildError(
|
|
186
|
+
'ERR_HARMONY_MANIFEST_DRIFT',
|
|
187
|
+
`Cannot stage the CNG manifest for isolated --check: ${source}`,
|
|
188
|
+
{ cause, file: source, operation: 'check' }
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const config = getConfig(project, {
|
|
193
|
+
isModdedConfig: true,
|
|
194
|
+
skipSDKVersionRequirement: true,
|
|
195
|
+
}).exp;
|
|
196
|
+
|
|
197
|
+
const harmony = normalizeHarmonyConfig(config);
|
|
198
|
+
const hash = hashSha256(stableHarmonyJson(harmony));
|
|
199
|
+
let identity = cng.managedIdentity;
|
|
200
|
+
let build = cng.build;
|
|
201
|
+
let signing = cng.signingConfigName;
|
|
202
|
+
|
|
203
|
+
if (hash === cng.inputs.configHash) {
|
|
204
|
+
// Reconstruct mutable re-entry state from authoritative inputs so a corrupt
|
|
205
|
+
// manifest cannot make isolated prebuild overwrite user-owned identities.
|
|
206
|
+
signing = harmony.signingConfigFile
|
|
207
|
+
? (await validateHarmonySigningConfigFile(project, harmony.signingConfigFile)).name
|
|
208
|
+
: null;
|
|
209
|
+
build = createHarmonyBuildDescriptor(harmony, signing);
|
|
210
|
+
identity = {
|
|
211
|
+
abilityName: build.identity.abilityName,
|
|
212
|
+
moduleName: build.identity.moduleName,
|
|
213
|
+
productName: build.identity.productName,
|
|
214
|
+
targetName: build.identity.targetName,
|
|
215
|
+
};
|
|
216
|
+
} else if (!await matchesIdentity(project, cng)) {
|
|
217
|
+
throw new HarmonyPrebuildError(
|
|
218
|
+
'ERR_HARMONY_MANIFEST_DRIFT',
|
|
219
|
+
'The CNG manifest identity does not match the generated Harmony project. Run prebuild to repair it before changing Harmony identity fields.',
|
|
220
|
+
{ operation: 'check' }
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const file = resolveHarmonyBuildPath(target, CngManifestPath);
|
|
225
|
+
|
|
226
|
+
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
|
227
|
+
await fs.promises.writeFile(file, stableHarmonyJson({
|
|
228
|
+
...cng,
|
|
229
|
+
build,
|
|
230
|
+
managedIdentity: identity,
|
|
231
|
+
signingConfigName: signing,
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function stageSigningAsync(
|
|
236
|
+
project: string,
|
|
237
|
+
temp: string
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
const config = getConfig(project, {
|
|
240
|
+
isModdedConfig: true,
|
|
241
|
+
skipSDKVersionRequirement: true,
|
|
242
|
+
}).exp;
|
|
243
|
+
|
|
244
|
+
const reference = (config as typeof config & {
|
|
245
|
+
harmony?: { signingConfigFile?: string };
|
|
246
|
+
}).harmony?.signingConfigFile;
|
|
247
|
+
|
|
248
|
+
if (!reference) return;
|
|
249
|
+
|
|
250
|
+
const signing = await validateHarmonySigningConfigFile(project, reference);
|
|
251
|
+
const inputs = [signing.file, ...Object.values(signing.materialFiles)];
|
|
252
|
+
|
|
253
|
+
for (const source of inputs) {
|
|
254
|
+
if (isInside(project, source)) continue;
|
|
255
|
+
|
|
256
|
+
const target = mirrorPath(temp, source);
|
|
257
|
+
|
|
258
|
+
if (!isInside(temp, target)) {
|
|
259
|
+
throw new HarmonyPrebuildError(
|
|
260
|
+
'ERR_HARMONY_SIGNING_INVALID',
|
|
261
|
+
`External Harmony signing input escapes the isolated --check workspace: ${source}`,
|
|
262
|
+
{ operation: 'check-signing' }
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await fs.promises.mkdir(path.dirname(target), { recursive: true });
|
|
267
|
+
await fs.promises.copyFile(source, target);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function stageAsync(
|
|
272
|
+
project: string,
|
|
273
|
+
expected: string,
|
|
274
|
+
temp: string
|
|
275
|
+
): Promise<void> {
|
|
276
|
+
await stageManifestAsync(project, expected);
|
|
277
|
+
await stageAutolinkingAsync(project, expected);
|
|
278
|
+
await stageSigningAsync(project, temp);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function canonicalizeAutolinkingAsync(
|
|
282
|
+
expected: string,
|
|
283
|
+
project: string,
|
|
284
|
+
cng: CngManifest
|
|
285
|
+
): Promise<void> {
|
|
286
|
+
const manifestFile = path.join(expected, '.expo/harmony/autolinking.json');
|
|
287
|
+
const ohpmPath = cng.build.nativeInputs.manifest;
|
|
288
|
+
const ohpmFile = resolveHarmonyBuildPath(expected, ohpmPath);
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const [manifest, ohpm, generatedRoot, projectRoot]
|
|
292
|
+
= await Promise.all([
|
|
293
|
+
fs.promises.readFile(manifestFile, 'utf8'),
|
|
294
|
+
fs.promises.readFile(ohpmFile, 'utf8'),
|
|
295
|
+
fs.promises.realpath(expected),
|
|
296
|
+
fs.promises.realpath(project),
|
|
297
|
+
]);
|
|
298
|
+
|
|
299
|
+
const result = canonicalizeAutolinkingArtifacts({
|
|
300
|
+
manifestSource: manifest,
|
|
301
|
+
ohPackageSource: ohpm,
|
|
302
|
+
generatedProjectRoot: generatedRoot,
|
|
303
|
+
canonicalProjectRoot: projectRoot,
|
|
304
|
+
harmonyProjectPath: resolveHarmonyBuildPath(projectRoot, cng.build.harmonyRoot),
|
|
305
|
+
});
|
|
306
|
+
const manifestHash = hashSha256(result.manifestSource);
|
|
307
|
+
const ohpmHash = hashSha256(result.ohPackageSource);
|
|
308
|
+
|
|
309
|
+
cng.inputs.autolinkingHash = manifestHash;
|
|
310
|
+
|
|
311
|
+
const manifestEntry = cng.managedFiles.find(
|
|
312
|
+
item => item.path === '.expo/harmony/autolinking.json'
|
|
313
|
+
);
|
|
314
|
+
const ohpmEntry = cng.managedFiles.find(
|
|
315
|
+
item => item.path === ohpmPath
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
if (manifestEntry) manifestEntry.sha256 = manifestHash;
|
|
319
|
+
if (ohpmEntry) ohpmEntry.sha256 = ohpmHash;
|
|
320
|
+
} catch (error) {
|
|
321
|
+
throw new HarmonyPrebuildError(
|
|
322
|
+
'ERR_HARMONY_MANIFEST_DRIFT',
|
|
323
|
+
'Cannot canonicalize the isolated autolinking artifacts.',
|
|
324
|
+
{ cause: error, file: manifestFile, operation: 'check' }
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function compareAsync(
|
|
330
|
+
project: string,
|
|
331
|
+
generated: string
|
|
332
|
+
): Promise<Result> {
|
|
333
|
+
const expected = await readManifestAsync(generated);
|
|
334
|
+
|
|
335
|
+
await canonicalizeAutolinkingAsync(
|
|
336
|
+
generated,
|
|
337
|
+
project,
|
|
338
|
+
expected
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const actual = await readManifestAsync(project);
|
|
342
|
+
const expectedFiles = new Map(expected.managedFiles.map(item => [item.path, item]));
|
|
343
|
+
const actualFiles = new Map(actual.managedFiles.map(item => [item.path, item]));
|
|
344
|
+
const changes: Change[] = [];
|
|
345
|
+
|
|
346
|
+
for (const [relative, descriptor] of expectedFiles) {
|
|
347
|
+
const file = path.join(project, ...relative.split('/'));
|
|
348
|
+
let hash = null;
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
hash = hashSha256(Uint8Array.from(await fs.promises.readFile(file)));
|
|
352
|
+
} catch (error) {
|
|
353
|
+
if (error.code !== 'ENOENT') {
|
|
354
|
+
throw new HarmonyPrebuildError(
|
|
355
|
+
error.code || 'ERR_HARMONY_MANIFEST_DRIFT',
|
|
356
|
+
error.message || `Cannot read a managed Harmony file: ${file}`,
|
|
357
|
+
{ cause: error, file, operation: error.operation || 'check' }
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (hash === null) changes.push({ path: relative, type: 'added' });
|
|
363
|
+
else if (hash !== descriptor.sha256) changes.push({ path: relative, type: 'changed' });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
for (const relative of actualFiles.keys()) {
|
|
367
|
+
if (!expectedFiles.has(relative)) changes.push({ path: relative, type: 'deleted' });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (!isDeepStrictEqual(expected, actual)) {
|
|
371
|
+
changes.push({ path: CngManifestPath, type: 'changed' });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
changes.sort((left, right) => (
|
|
375
|
+
left.path.localeCompare(right.path, 'en') || left.type.localeCompare(right.type, 'en')
|
|
376
|
+
));
|
|
377
|
+
|
|
378
|
+
return { changes, clean: changes.length === 0, expected };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export {
|
|
382
|
+
compareAsync,
|
|
383
|
+
readManifestAsync,
|
|
384
|
+
readManifestIfPresentAsync,
|
|
385
|
+
stageAsync,
|
|
386
|
+
};
|
|
387
|
+
export type {
|
|
388
|
+
Change,
|
|
389
|
+
Result,
|
|
390
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { HarmonyPrebuildError } from './errors';
|
|
6
|
+
import { resolve as resolveTemplate } from './template';
|
|
7
|
+
|
|
8
|
+
const resolveModule = createRequire(__filename).resolve;
|
|
9
|
+
|
|
10
|
+
function resolvePackageFile(root, name, relative) {
|
|
11
|
+
const logical = path.join(root, 'node_modules', ...name.split('/'), relative);
|
|
12
|
+
|
|
13
|
+
if (fs.existsSync(logical)) return logical;
|
|
14
|
+
|
|
15
|
+
let json;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
json = resolveModule(`${name}/package.json`, { paths: [root] });
|
|
19
|
+
} catch (cause) {
|
|
20
|
+
throw new HarmonyPrebuildError(
|
|
21
|
+
'ERR_HARMONY_DEPENDENCY_MISSING',
|
|
22
|
+
`Unable to resolve ${name} from the project.`,
|
|
23
|
+
{ cause, operation: 'resolve-dependency' }
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const resolved = path.join(path.dirname(json), relative);
|
|
28
|
+
|
|
29
|
+
if (!fs.existsSync(resolved)) {
|
|
30
|
+
throw new HarmonyPrebuildError(
|
|
31
|
+
'ERR_HARMONY_DEPENDENCY_MISSING',
|
|
32
|
+
`${name} is missing ${relative}.`,
|
|
33
|
+
{ file: resolved, operation: 'resolve-dependency' }
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return resolved;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolvePackageVersion(root, name) {
|
|
41
|
+
const json = resolvePackageFile(root, name, 'package.json');
|
|
42
|
+
|
|
43
|
+
let version;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
version = JSON.parse(fs.readFileSync(json, 'utf8')).version;
|
|
47
|
+
} catch (cause) {
|
|
48
|
+
throw new HarmonyPrebuildError(
|
|
49
|
+
'ERR_HARMONY_DEPENDENCY_MISSING',
|
|
50
|
+
`Cannot read ${name} version.`,
|
|
51
|
+
{ cause, file: json, operation: 'resolve-dependency' }
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof version !== 'string' || !version) {
|
|
56
|
+
throw new HarmonyPrebuildError(
|
|
57
|
+
'ERR_HARMONY_DEPENDENCY_MISSING',
|
|
58
|
+
`${name} has no package version.`,
|
|
59
|
+
{ file: json, operation: 'resolve-dependency' }
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return version;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolveTemplateFile(relative) {
|
|
67
|
+
const resolved = path.join(resolveTemplate().root, relative);
|
|
68
|
+
|
|
69
|
+
if (!fs.existsSync(resolved)) {
|
|
70
|
+
throw new HarmonyPrebuildError(
|
|
71
|
+
'ERR_HARMONY_DEPENDENCY_MISSING',
|
|
72
|
+
`@expo-harmony/template is missing ${relative}.`,
|
|
73
|
+
{ file: resolved, operation: 'resolve-dependency' }
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return resolved;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function readTemplateSource(relative) {
|
|
81
|
+
return fs.promises.readFile(resolveTemplateFile(`harmony/${relative}`), 'utf8');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveRnohHvigorPlugin(root) {
|
|
85
|
+
const json = resolvePackageFile(root, '@react-native-oh/react-native-harmony-cli', 'package.json');
|
|
86
|
+
const harmony = path.join(path.dirname(json), 'harmony');
|
|
87
|
+
|
|
88
|
+
let candidates;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
candidates = fs.readdirSync(harmony)
|
|
92
|
+
.filter(name => /^rnoh-hvigor-plugin-.+\.tgz$/u.test(name))
|
|
93
|
+
.sort();
|
|
94
|
+
} catch (cause) {
|
|
95
|
+
throw new HarmonyPrebuildError(
|
|
96
|
+
'ERR_HARMONY_DEPENDENCY_MISSING',
|
|
97
|
+
'@react-native-oh/react-native-harmony-cli has no Harmony Hvigor plugin directory.',
|
|
98
|
+
{ cause, file: harmony, operation: 'resolve-dependency' }
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (candidates.length !== 1) {
|
|
103
|
+
throw new HarmonyPrebuildError(
|
|
104
|
+
'ERR_HARMONY_DEPENDENCY_MISSING',
|
|
105
|
+
'@react-native-oh/react-native-harmony-cli must contain exactly one RNOH Hvigor plugin archive.',
|
|
106
|
+
{ file: harmony, operation: 'resolve-dependency' }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return path.join(harmony, candidates[0]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function toRelativeDependency(directory, file) {
|
|
114
|
+
return path.relative(directory, file).split(path.sep).join('/');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export {
|
|
118
|
+
readTemplateSource,
|
|
119
|
+
resolvePackageVersion,
|
|
120
|
+
resolveRnohHvigorPlugin,
|
|
121
|
+
resolveTemplateFile,
|
|
122
|
+
toRelativeDependency,
|
|
123
|
+
};
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface HarmonyPrebuildErrorOptions {
|
|
2
|
+
cause?: unknown;
|
|
3
|
+
file?: string;
|
|
4
|
+
operation?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
class HarmonyPrebuildError extends Error {
|
|
8
|
+
readonly code: string;
|
|
9
|
+
readonly file?: string;
|
|
10
|
+
readonly operation: string;
|
|
11
|
+
|
|
12
|
+
constructor(code: string, message: string, options: HarmonyPrebuildErrorOptions = {}) {
|
|
13
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
14
|
+
|
|
15
|
+
this.name = 'HarmonyPrebuildError';
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.operation = options.operation || 'prebuild';
|
|
18
|
+
if (options.file) this.file = options.file;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { HarmonyPrebuildError };
|
|
23
|
+
export type { HarmonyPrebuildErrorOptions };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
async function readTextIfExistsAsync(file: string): Promise<string | null> {
|
|
5
|
+
try {
|
|
6
|
+
return await fs.promises.readFile(file, 'utf8');
|
|
7
|
+
} catch (error) {
|
|
8
|
+
if (error.code === 'ENOENT') return null;
|
|
9
|
+
return Promise.reject(error);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function writeFileAtomicAsync(file: string, content: string) {
|
|
14
|
+
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
|
15
|
+
|
|
16
|
+
const temp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.tmp`);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
await fs.promises.writeFile(temp, content);
|
|
20
|
+
await fs.promises.rename(temp, file);
|
|
21
|
+
} finally {
|
|
22
|
+
await fs.promises.rm(temp, { force: true }).catch(() => {});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function ensureGitignoreEntryAsync(file: string, entry: string) {
|
|
27
|
+
const content = await readTextIfExistsAsync(file) ?? '';
|
|
28
|
+
const text = content.replace(/\r\n?/g, '\n');
|
|
29
|
+
|
|
30
|
+
if (text.split('\n').includes(entry)) return;
|
|
31
|
+
|
|
32
|
+
const next = `${text.replace(/\n*$/, '')}${text.trim() ? '\n' : ''}${entry}\n`;
|
|
33
|
+
|
|
34
|
+
await writeFileAtomicAsync(file, next);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
ensureGitignoreEntryAsync,
|
|
39
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export { HarmonyPrebuildPlugin as default } from './plugin';
|
|
2
|
+
export {
|
|
3
|
+
BuildDescriptorSchemaVersion,
|
|
4
|
+
BuildModes,
|
|
5
|
+
HarmonyPlatformDirectory,
|
|
6
|
+
HarmonyTemplateMarker,
|
|
7
|
+
createHarmonyBuildDescriptor,
|
|
8
|
+
harmonyModuleSourcePath,
|
|
9
|
+
resolveHarmonyBuildPath,
|
|
10
|
+
validateHarmonyBuildDescriptor,
|
|
11
|
+
type HarmonyBuildDescriptor,
|
|
12
|
+
type HarmonyBuildMode,
|
|
13
|
+
type HarmonyBuildVariantDescriptor,
|
|
14
|
+
} from './buildDescriptor';
|
|
15
|
+
export { HarmonyPrebuildError } from './errors';
|
|
16
|
+
export {
|
|
17
|
+
CngManifestPath,
|
|
18
|
+
createCngManifest,
|
|
19
|
+
validateCngManifest,
|
|
20
|
+
type CngManifest,
|
|
21
|
+
} from './manifest';
|
|
22
|
+
export { isRnohAutolinkingDisabled } from './native';
|
|
23
|
+
export { validateHarmonySigningConfigFile, type HarmonySigningConfig } from './signing';
|
|
24
|
+
export { withHarmonyPrebuildConfig } from './withHarmonyPrebuildConfig';
|
|
25
|
+
export type { HarmonyPrebuildOptions } from './withHarmonyPrebuildConfig';
|