@expo-harmony/config-plugins 55.0.10-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 +64 -0
- package/build/index.js +49 -0
- package/package.json +62 -0
- package/src/config.ts +79 -0
- package/src/errors.ts +21 -0
- package/src/files.ts +79 -0
- package/src/index.ts +71 -0
- package/src/mods.ts +596 -0
- package/src/nativeInputs.ts +113 -0
- package/src/normalizeConfig.ts +417 -0
- package/src/ownership.ts +147 -0
- package/src/paths.ts +209 -0
- package/tsconfig.json +17 -0
package/src/mods.ts
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
withBaseMod, withMod,
|
|
6
|
+
type ConfigPlugin, type ExportedConfig, type ExportedConfigWithProps, type ModPlatform,
|
|
7
|
+
} from '@expo/config-plugins';
|
|
8
|
+
import type { ExpoConfig } from '@expo/config-types';
|
|
9
|
+
|
|
10
|
+
import { HarmonyConfigPluginError } from './errors';
|
|
11
|
+
import { atomicWrite, readJson5, readText, writeJson5 } from './files';
|
|
12
|
+
import {
|
|
13
|
+
ManagedPaths, ProjectPaths, ResourcePaths,
|
|
14
|
+
resolveHarmonyPath, resolveProjectPath, toPosixRelative,
|
|
15
|
+
} from './paths';
|
|
16
|
+
|
|
17
|
+
export type HarmonyModName
|
|
18
|
+
= | 'dangerous'
|
|
19
|
+
| 'reactNativeConfig'
|
|
20
|
+
| 'appJson'
|
|
21
|
+
| 'projectBuildProfile'
|
|
22
|
+
| 'rootOhPackage'
|
|
23
|
+
| 'rootHvigor'
|
|
24
|
+
| 'nativeInputsStamp'
|
|
25
|
+
| 'hvigorConfig'
|
|
26
|
+
| 'entryBuildProfile'
|
|
27
|
+
| 'entryOhPackage'
|
|
28
|
+
| 'entryHvigor'
|
|
29
|
+
| 'moduleJson'
|
|
30
|
+
| 'strings'
|
|
31
|
+
| 'colors'
|
|
32
|
+
| 'media'
|
|
33
|
+
| 'profiles'
|
|
34
|
+
| 'entryAbility'
|
|
35
|
+
| 'indexPage'
|
|
36
|
+
| 'worker'
|
|
37
|
+
| 'arktsPackageProvider'
|
|
38
|
+
| 'cppPackageProvider'
|
|
39
|
+
| 'cmakeLists'
|
|
40
|
+
| 'autolinking'
|
|
41
|
+
| 'manifest';
|
|
42
|
+
|
|
43
|
+
export type HarmonyModAction<T = unknown> = (
|
|
44
|
+
config: ExportedConfigWithProps<T>
|
|
45
|
+
) => ExportedConfigWithProps<T> | Promise<ExportedConfigWithProps<T>>;
|
|
46
|
+
|
|
47
|
+
export type HarmonyJson = Record<string, unknown>;
|
|
48
|
+
export type HarmonyResourceMap = Record<string, HarmonyJson>;
|
|
49
|
+
export type HarmonyMediaDescriptor
|
|
50
|
+
= | {
|
|
51
|
+
source: string;
|
|
52
|
+
content?: never;
|
|
53
|
+
replaceBase?: boolean;
|
|
54
|
+
}
|
|
55
|
+
| {
|
|
56
|
+
source?: never;
|
|
57
|
+
content: string | Uint8Array;
|
|
58
|
+
replaceBase?: boolean;
|
|
59
|
+
};
|
|
60
|
+
export type HarmonyMediaMap = Record<string, Record<string, HarmonyMediaDescriptor>>;
|
|
61
|
+
|
|
62
|
+
type FileModName = keyof typeof ManagedPaths | keyof typeof ProjectPaths;
|
|
63
|
+
type ResourceModName = keyof typeof ResourcePaths;
|
|
64
|
+
type FileResult = HarmonyJson | string;
|
|
65
|
+
type ResourceResult = HarmonyResourceMap | HarmonyMediaMap;
|
|
66
|
+
type ModEntry = {
|
|
67
|
+
isProvider?: boolean;
|
|
68
|
+
expoHarmonyLateActions?: HarmonyModAction[];
|
|
69
|
+
};
|
|
70
|
+
type ModTable = Record<string, Record<string, ModEntry> | undefined>;
|
|
71
|
+
|
|
72
|
+
interface ManagedConfig {
|
|
73
|
+
_internal?: ExpoConfig['_internal'];
|
|
74
|
+
modRequest: { projectRoot: string };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface MediaWrite {
|
|
78
|
+
content: string | Uint8Array;
|
|
79
|
+
directory: string;
|
|
80
|
+
name: string;
|
|
81
|
+
replaceBase: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const JsonMods = new Set<FileModName>([
|
|
85
|
+
'appJson',
|
|
86
|
+
'projectBuildProfile',
|
|
87
|
+
'rootOhPackage',
|
|
88
|
+
'hvigorConfig',
|
|
89
|
+
'entryBuildProfile',
|
|
90
|
+
'entryOhPackage',
|
|
91
|
+
'moduleJson',
|
|
92
|
+
'profiles',
|
|
93
|
+
]);
|
|
94
|
+
const TextMods = new Set<FileModName>([
|
|
95
|
+
'reactNativeConfig',
|
|
96
|
+
'rootHvigor',
|
|
97
|
+
'nativeInputsStamp',
|
|
98
|
+
'entryHvigor',
|
|
99
|
+
'entryAbility',
|
|
100
|
+
'indexPage',
|
|
101
|
+
'worker',
|
|
102
|
+
'arktsPackageProvider',
|
|
103
|
+
'cppPackageProvider',
|
|
104
|
+
'cmakeLists',
|
|
105
|
+
]);
|
|
106
|
+
const ResourceMods = new Set<HarmonyModName>(['strings', 'colors', 'media']);
|
|
107
|
+
const VirtualMods = new Set<HarmonyModName>(['dangerous', 'autolinking', 'manifest']);
|
|
108
|
+
const ResourceModNames: readonly ResourceModName[] = ['strings', 'colors', 'media'];
|
|
109
|
+
|
|
110
|
+
export const HarmonyModNames: readonly HarmonyModName[] = Object.freeze([
|
|
111
|
+
'dangerous',
|
|
112
|
+
'reactNativeConfig',
|
|
113
|
+
'appJson',
|
|
114
|
+
'projectBuildProfile',
|
|
115
|
+
'rootOhPackage',
|
|
116
|
+
'rootHvigor',
|
|
117
|
+
'nativeInputsStamp',
|
|
118
|
+
'hvigorConfig',
|
|
119
|
+
'entryBuildProfile',
|
|
120
|
+
'entryOhPackage',
|
|
121
|
+
'entryHvigor',
|
|
122
|
+
'moduleJson',
|
|
123
|
+
'strings',
|
|
124
|
+
'colors',
|
|
125
|
+
'media',
|
|
126
|
+
'profiles',
|
|
127
|
+
'entryAbility',
|
|
128
|
+
'indexPage',
|
|
129
|
+
'worker',
|
|
130
|
+
'arktsPackageProvider',
|
|
131
|
+
'cppPackageProvider',
|
|
132
|
+
'cmakeLists',
|
|
133
|
+
'autolinking',
|
|
134
|
+
'manifest',
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
export function recordManagedFile(config: ManagedConfig, file: string, owner: string): void {
|
|
138
|
+
config._internal ??= {};
|
|
139
|
+
config._internal.harmonyManagedFiles ??= [];
|
|
140
|
+
|
|
141
|
+
const entry = toPosixRelative(config.modRequest.projectRoot, file);
|
|
142
|
+
const files = config._internal.harmonyManagedFiles.filter((item: { path: string }) => item.path !== entry);
|
|
143
|
+
|
|
144
|
+
files.push({ path: entry, owner });
|
|
145
|
+
files.sort((left: { path: string }, right: { path: string }) => left.path.localeCompare(right.path, 'en'));
|
|
146
|
+
config._internal.harmonyManagedFiles = files;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function assertModResults(modName: HarmonyModName, value: unknown, kind: 'json' | 'resource' | 'text'): void {
|
|
150
|
+
const valid = kind === 'text'
|
|
151
|
+
? typeof value === 'string'
|
|
152
|
+
: value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
153
|
+
|
|
154
|
+
if (!valid) {
|
|
155
|
+
throw new HarmonyConfigPluginError(
|
|
156
|
+
'ERR_HARMONY_MOD_RESULTS_INVALID',
|
|
157
|
+
`harmony.${modName} must return ${kind === 'text' ? 'a string' : 'an object'} modResults.`,
|
|
158
|
+
{ operation: `harmony.${modName}.write` }
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function withFileProvider(config: ExpoConfig, modName: FileModName): ExpoConfig {
|
|
164
|
+
return withBaseMod<FileResult>(config, {
|
|
165
|
+
platform: 'harmony' as ModPlatform,
|
|
166
|
+
mod: modName,
|
|
167
|
+
isProvider: true,
|
|
168
|
+
isIntrospective: JsonMods.has(modName),
|
|
169
|
+
saveToInternal: JsonMods.has(modName),
|
|
170
|
+
async action(value) {
|
|
171
|
+
const { nextMod, ...modRequest } = value.modRequest;
|
|
172
|
+
const isProjectFile = modName in ProjectPaths;
|
|
173
|
+
const root = isProjectFile ? modRequest.projectRoot : modRequest.platformProjectRoot;
|
|
174
|
+
const file = isProjectFile
|
|
175
|
+
? await resolveProjectPath(root, modName as keyof typeof ProjectPaths)
|
|
176
|
+
: await resolveHarmonyPath(root, ManagedPaths[modName as keyof typeof ManagedPaths]);
|
|
177
|
+
const exists = fs.existsSync(file);
|
|
178
|
+
const data = JsonMods.has(modName)
|
|
179
|
+
? await readJson5<HarmonyJson>(file, {}, modName)
|
|
180
|
+
: await readText(file);
|
|
181
|
+
|
|
182
|
+
const request = { ...modRequest, modFile: file, modFileExists: exists } as typeof value.modRequest;
|
|
183
|
+
const result = await nextMod!({ ...value, modRequest: request, modResults: data });
|
|
184
|
+
|
|
185
|
+
assertModResults(modName, result.modResults, JsonMods.has(modName) ? 'json' : 'text');
|
|
186
|
+
|
|
187
|
+
if (!result.modRequest.introspect) {
|
|
188
|
+
if (typeof result.modResults === 'string') {
|
|
189
|
+
await atomicWrite(file, result.modResults.replace(/\r\n?/g, '\n').replace(/\n?$/, '\n'));
|
|
190
|
+
} else {
|
|
191
|
+
await writeJson5(file, result.modResults);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
recordManagedFile(result, file, modName);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return result;
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function readResourceMap(
|
|
203
|
+
root: string,
|
|
204
|
+
entries: Readonly<Record<string, string>>,
|
|
205
|
+
modName: ResourceModName
|
|
206
|
+
): Promise<HarmonyResourceMap> {
|
|
207
|
+
const resources: HarmonyResourceMap = {};
|
|
208
|
+
|
|
209
|
+
for (const [scope, relative] of Object.entries(entries)) {
|
|
210
|
+
const file = await resolveHarmonyPath(root, relative);
|
|
211
|
+
resources[scope] = await readJson5<HarmonyJson>(file, {}, modName);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return resources;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function readMediaMap(root: string): Promise<HarmonyMediaMap> {
|
|
218
|
+
const media: HarmonyMediaMap = {};
|
|
219
|
+
|
|
220
|
+
for (const [scope, relative] of Object.entries(ResourcePaths.media)) {
|
|
221
|
+
const directory = await resolveHarmonyPath(root, relative);
|
|
222
|
+
|
|
223
|
+
media[scope] = {};
|
|
224
|
+
let names: string[] = [];
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
names = await fs.promises.readdir(directory);
|
|
228
|
+
} catch (cause) {
|
|
229
|
+
if ((cause as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
230
|
+
throw new HarmonyConfigPluginError(
|
|
231
|
+
'ERR_HARMONY_RESOURCE_READ',
|
|
232
|
+
`Cannot read Harmony media directory ${directory}: ${(cause as Error).message}`,
|
|
233
|
+
{ cause, file: directory, operation: 'harmony.media.read' }
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (const name of names.sort()) {
|
|
239
|
+
const file = path.join(directory, name);
|
|
240
|
+
|
|
241
|
+
if ((await fs.promises.lstat(file)).isFile()) media[scope][name] = { source: file };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return media;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function writeMediaMap(
|
|
249
|
+
root: string,
|
|
250
|
+
media: HarmonyMediaMap,
|
|
251
|
+
config: ManagedConfig,
|
|
252
|
+
previous: Record<string, string[]> = {}
|
|
253
|
+
): Promise<void> {
|
|
254
|
+
const writes: MediaWrite[] = [];
|
|
255
|
+
|
|
256
|
+
for (const [scope, files] of Object.entries(media || {})) {
|
|
257
|
+
if (!(scope in ResourcePaths.media)) continue;
|
|
258
|
+
|
|
259
|
+
if (!files || typeof files !== 'object' || Array.isArray(files)) {
|
|
260
|
+
throw new HarmonyConfigPluginError(
|
|
261
|
+
'ERR_HARMONY_CONFIG_INVALID',
|
|
262
|
+
`Harmony media scope ${scope} must be an object.`,
|
|
263
|
+
{ operation: 'harmony.media.write' }
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const directory = await resolveHarmonyPath(root, ResourcePaths.media[scope as keyof typeof ResourcePaths.media]);
|
|
268
|
+
|
|
269
|
+
for (const [name, descriptor] of Object.entries(files)) {
|
|
270
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(name) || name.includes('..')) {
|
|
271
|
+
throw new HarmonyConfigPluginError(
|
|
272
|
+
'ERR_HARMONY_CONFIG_INVALID',
|
|
273
|
+
`Invalid Harmony media file name: ${name}`,
|
|
274
|
+
{ operation: 'harmony.media.write' }
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
if (!descriptor || typeof descriptor !== 'object' || Array.isArray(descriptor)) {
|
|
278
|
+
throw new HarmonyConfigPluginError(
|
|
279
|
+
'ERR_HARMONY_CONFIG_INVALID',
|
|
280
|
+
`Harmony media ${scope}/${name} must use a descriptor object.`,
|
|
281
|
+
{ operation: 'harmony.media.write' }
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const item = descriptor as Partial<HarmonyMediaDescriptor>;
|
|
286
|
+
const hasSource = Object.hasOwn(item, 'source');
|
|
287
|
+
const hasContent = Object.hasOwn(item, 'content');
|
|
288
|
+
|
|
289
|
+
if (hasSource === hasContent) {
|
|
290
|
+
throw new HarmonyConfigPluginError(
|
|
291
|
+
'ERR_HARMONY_CONFIG_INVALID',
|
|
292
|
+
`Harmony media ${scope}/${name} must define exactly one of source or content.`,
|
|
293
|
+
{ operation: 'harmony.media.write' }
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (item.replaceBase !== undefined && typeof item.replaceBase !== 'boolean') {
|
|
297
|
+
throw new HarmonyConfigPluginError(
|
|
298
|
+
'ERR_HARMONY_CONFIG_INVALID',
|
|
299
|
+
`Harmony media ${scope}/${name} replaceBase must be a boolean.`,
|
|
300
|
+
{ operation: 'harmony.media.write' }
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let content: string | Uint8Array;
|
|
305
|
+
|
|
306
|
+
if (hasSource) {
|
|
307
|
+
if (typeof item.source !== 'string' || item.source.length === 0) {
|
|
308
|
+
throw new HarmonyConfigPluginError(
|
|
309
|
+
'ERR_HARMONY_CONFIG_INVALID',
|
|
310
|
+
`Harmony media ${scope}/${name} source must be a non-empty path.`,
|
|
311
|
+
{ operation: 'harmony.media.write' }
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
content = Uint8Array.from(await fs.promises.readFile(item.source));
|
|
316
|
+
} else {
|
|
317
|
+
if (typeof item.content !== 'string' && !(item.content instanceof Uint8Array)) {
|
|
318
|
+
throw new HarmonyConfigPluginError(
|
|
319
|
+
'ERR_HARMONY_CONFIG_INVALID',
|
|
320
|
+
`Harmony media ${scope}/${name} content must be a string or Uint8Array.`,
|
|
321
|
+
{ operation: 'harmony.media.write' }
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
content = item.content;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
writes.push({
|
|
329
|
+
content,
|
|
330
|
+
directory,
|
|
331
|
+
name,
|
|
332
|
+
replaceBase: item.replaceBase === true,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Resolve and read every desired input before removing stale resources. A
|
|
338
|
+
// malformed descriptor or missing source therefore cannot partially mutate
|
|
339
|
+
// an otherwise valid generated project.
|
|
340
|
+
for (const [scope, names] of Object.entries(previous)) {
|
|
341
|
+
if (!(scope in ResourcePaths.media)) continue;
|
|
342
|
+
const directory = await resolveHarmonyPath(
|
|
343
|
+
root,
|
|
344
|
+
ResourcePaths.media[scope as keyof typeof ResourcePaths.media]
|
|
345
|
+
);
|
|
346
|
+
const next = media?.[scope];
|
|
347
|
+
|
|
348
|
+
for (const name of names) {
|
|
349
|
+
if (!next || typeof next !== 'object' || !Object.hasOwn(next, name)) {
|
|
350
|
+
const file = await resolveHarmonyPath(directory, name);
|
|
351
|
+
|
|
352
|
+
await fs.promises.rm(file, { force: true });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const { content, directory, name, replaceBase } of writes) {
|
|
358
|
+
await fs.promises.mkdir(directory, { recursive: true });
|
|
359
|
+
|
|
360
|
+
if (replaceBase) {
|
|
361
|
+
const base = path.parse(name).name;
|
|
362
|
+
|
|
363
|
+
for (const oldName of await fs.promises.readdir(directory)) {
|
|
364
|
+
if (path.parse(oldName).name === base && oldName !== name) {
|
|
365
|
+
await fs.promises.rm(path.join(directory, oldName), { force: true });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const file = await resolveHarmonyPath(directory, name);
|
|
371
|
+
|
|
372
|
+
await atomicWrite(file, content);
|
|
373
|
+
recordManagedFile(config, file, 'media');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function withResourceProvider(config: ExpoConfig, modName: ResourceModName): ExpoConfig {
|
|
378
|
+
const isIntrospective = modName !== 'media';
|
|
379
|
+
|
|
380
|
+
return withBaseMod<ResourceResult>(config, {
|
|
381
|
+
platform: 'harmony' as ModPlatform,
|
|
382
|
+
mod: modName,
|
|
383
|
+
isProvider: true,
|
|
384
|
+
isIntrospective,
|
|
385
|
+
saveToInternal: isIntrospective,
|
|
386
|
+
async action(value) {
|
|
387
|
+
const { nextMod, ...modRequest } = value.modRequest;
|
|
388
|
+
const root = modRequest.platformProjectRoot;
|
|
389
|
+
const data = modName === 'media'
|
|
390
|
+
? await readMediaMap(root)
|
|
391
|
+
: await readResourceMap(root, ResourcePaths[modName], modName);
|
|
392
|
+
const previous = modName === 'media'
|
|
393
|
+
? Object.fromEntries(
|
|
394
|
+
Object.entries(data).map(([scope, files]) => [scope, Object.keys(files)])
|
|
395
|
+
)
|
|
396
|
+
: {};
|
|
397
|
+
|
|
398
|
+
const result = await nextMod!({ ...value, modRequest, modResults: data });
|
|
399
|
+
|
|
400
|
+
assertModResults(modName, result.modResults, 'resource');
|
|
401
|
+
|
|
402
|
+
if (!result.modRequest.introspect) {
|
|
403
|
+
if (modName === 'media') {
|
|
404
|
+
await writeMediaMap(root, result.modResults as HarmonyMediaMap, result, previous);
|
|
405
|
+
} else {
|
|
406
|
+
const paths = ResourcePaths[modName];
|
|
407
|
+
|
|
408
|
+
for (const [scope, resource] of Object.entries(result.modResults)) {
|
|
409
|
+
if (!(scope in paths)) continue;
|
|
410
|
+
|
|
411
|
+
// Harmony's resource compiler requires the JSON root to contain
|
|
412
|
+
// exactly one resource-kind member. An empty scope serialized as
|
|
413
|
+
// `{}` is invalid, so omit it until a plugin contributes content.
|
|
414
|
+
if (!resource || typeof resource !== 'object' || Array.isArray(resource)) {
|
|
415
|
+
throw new HarmonyConfigPluginError(
|
|
416
|
+
'ERR_HARMONY_CONFIG_INVALID',
|
|
417
|
+
`Harmony ${modName} scope ${scope} must be an object.`,
|
|
418
|
+
{ operation: `harmony.${modName}.write` }
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const file = await resolveHarmonyPath(root, paths[scope as keyof typeof paths]);
|
|
423
|
+
|
|
424
|
+
if (Object.keys(resource).length === 0) {
|
|
425
|
+
await fs.promises.rm(file, { force: true });
|
|
426
|
+
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
await writeJson5(file, resource);
|
|
431
|
+
recordManagedFile(result, file, modName);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return result;
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function withVirtualProvider(config: ExpoConfig, modName: HarmonyModName): ExpoConfig {
|
|
442
|
+
return withBaseMod<null>(config, {
|
|
443
|
+
platform: 'harmony' as ModPlatform,
|
|
444
|
+
mod: modName,
|
|
445
|
+
isProvider: true,
|
|
446
|
+
async action(value) {
|
|
447
|
+
const { nextMod, ...modRequest } = value.modRequest;
|
|
448
|
+
|
|
449
|
+
return nextMod!({ ...value, modRequest, modResults: null });
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function withProvider(config: ExpoConfig, modName: HarmonyModName): ExpoConfig {
|
|
455
|
+
const actions: HarmonyModAction[] = [];
|
|
456
|
+
|
|
457
|
+
config = withMod(config, {
|
|
458
|
+
platform: 'harmony' as ModPlatform,
|
|
459
|
+
mod: modName,
|
|
460
|
+
async action(value) {
|
|
461
|
+
let result = value;
|
|
462
|
+
|
|
463
|
+
// withMod is a stack: the last registered plugin runs first. Preserve
|
|
464
|
+
// that ordering for Harmony plugins registered after the provider.
|
|
465
|
+
for (let index = actions.length - 1; index >= 0; index -= 1) {
|
|
466
|
+
result = await actions[index](result);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return result;
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
if (JsonMods.has(modName as FileModName) || TextMods.has(modName as FileModName)) {
|
|
474
|
+
config = withFileProvider(config, modName as FileModName);
|
|
475
|
+
} else if (ResourceMods.has(modName)) {
|
|
476
|
+
config = withResourceProvider(config, modName as ResourceModName);
|
|
477
|
+
} else if (VirtualMods.has(modName)) {
|
|
478
|
+
config = withVirtualProvider(config, modName);
|
|
479
|
+
} else {
|
|
480
|
+
throw new HarmonyConfigPluginError(
|
|
481
|
+
'ERR_HARMONY_MOD_NOT_REGISTERED',
|
|
482
|
+
`Unknown Harmony mod: ${modName}`,
|
|
483
|
+
{ operation: 'register-base-mods' }
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const mods = (config as ExportedConfig).mods as unknown as ModTable | undefined;
|
|
488
|
+
const provider = mods?.harmony?.[modName];
|
|
489
|
+
|
|
490
|
+
if (provider?.isProvider) provider.expoHarmonyLateActions = actions;
|
|
491
|
+
|
|
492
|
+
return config;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function withHarmonyBaseMods(config: ExpoConfig): ExpoConfig {
|
|
496
|
+
for (const modName of HarmonyModNames) {
|
|
497
|
+
const mods = (config as ExportedConfig).mods as unknown as ModTable;
|
|
498
|
+
|
|
499
|
+
if (!mods?.harmony?.[modName]?.isProvider) config = withProvider(config, modName);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const mods = (config as ExportedConfig).mods as unknown as ModTable;
|
|
503
|
+
const table = mods.harmony!;
|
|
504
|
+
|
|
505
|
+
mods.harmony = Object.fromEntries([
|
|
506
|
+
...HarmonyModNames.filter(name => table[name]).map(name => [name, table[name]]),
|
|
507
|
+
...Object.entries(table).filter(([name]) => !HarmonyModNames.includes(name as HarmonyModName)),
|
|
508
|
+
]);
|
|
509
|
+
|
|
510
|
+
return config;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export function withHarmonyMod<T = unknown>(
|
|
514
|
+
config: ExpoConfig,
|
|
515
|
+
tuple: [HarmonyModName, HarmonyModAction<T>]
|
|
516
|
+
): ExpoConfig {
|
|
517
|
+
if (!Array.isArray(tuple) || tuple.length !== 2) {
|
|
518
|
+
throw new TypeError('withHarmonyMod expects [modName, action].');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const [modName, action] = tuple;
|
|
522
|
+
if (!HarmonyModNames.includes(modName) || typeof action !== 'function') {
|
|
523
|
+
throw new HarmonyConfigPluginError(
|
|
524
|
+
'ERR_HARMONY_MOD_NOT_REGISTERED',
|
|
525
|
+
`Cannot register Harmony mod: ${modName}`,
|
|
526
|
+
{ operation: 'register-mod' }
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const mods = (config as ExportedConfig).mods as unknown as ModTable | undefined;
|
|
531
|
+
const provider = mods?.harmony?.[modName];
|
|
532
|
+
|
|
533
|
+
if (provider?.isProvider && provider.expoHarmonyLateActions) {
|
|
534
|
+
provider.expoHarmonyLateActions.push(action as HarmonyModAction);
|
|
535
|
+
return config;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return withMod<T>(config, { platform: 'harmony' as ModPlatform, mod: modName, action });
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export const withReactNativeConfig: ConfigPlugin<HarmonyModAction<string>>
|
|
542
|
+
= (config, action) => withHarmonyMod(config, ['reactNativeConfig', action]);
|
|
543
|
+
export const withAppJson: ConfigPlugin<HarmonyModAction<HarmonyJson>>
|
|
544
|
+
= (config, action) => withHarmonyMod(config, ['appJson', action]);
|
|
545
|
+
export const withProjectBuildProfile: ConfigPlugin<HarmonyModAction<HarmonyJson>>
|
|
546
|
+
= (config, action) => withHarmonyMod(config, ['projectBuildProfile', action]);
|
|
547
|
+
export const withRootOhPackage: ConfigPlugin<HarmonyModAction<HarmonyJson>>
|
|
548
|
+
= (config, action) => withHarmonyMod(config, ['rootOhPackage', action]);
|
|
549
|
+
export const withRootHvigor: ConfigPlugin<HarmonyModAction<string>>
|
|
550
|
+
= (config, action) => withHarmonyMod(config, ['rootHvigor', action]);
|
|
551
|
+
export const withNativeInputsStamp: ConfigPlugin<HarmonyModAction<string>>
|
|
552
|
+
= (config, action) => withHarmonyMod(config, ['nativeInputsStamp', action]);
|
|
553
|
+
export const withHvigorConfig: ConfigPlugin<HarmonyModAction<HarmonyJson>>
|
|
554
|
+
= (config, action) => withHarmonyMod(config, ['hvigorConfig', action]);
|
|
555
|
+
export const withEntryBuildProfile: ConfigPlugin<HarmonyModAction<HarmonyJson>>
|
|
556
|
+
= (config, action) => withHarmonyMod(config, ['entryBuildProfile', action]);
|
|
557
|
+
export const withEntryOhPackage: ConfigPlugin<HarmonyModAction<HarmonyJson>>
|
|
558
|
+
= (config, action) => withHarmonyMod(config, ['entryOhPackage', action]);
|
|
559
|
+
export const withEntryHvigor: ConfigPlugin<HarmonyModAction<string>>
|
|
560
|
+
= (config, action) => withHarmonyMod(config, ['entryHvigor', action]);
|
|
561
|
+
export const withModuleJson: ConfigPlugin<HarmonyModAction<HarmonyJson>>
|
|
562
|
+
= (config, action) => withHarmonyMod(config, ['moduleJson', action]);
|
|
563
|
+
export const withStrings: ConfigPlugin<HarmonyModAction<HarmonyResourceMap>>
|
|
564
|
+
= (config, action) => withHarmonyMod(config, ['strings', action]);
|
|
565
|
+
export const withColors: ConfigPlugin<HarmonyModAction<HarmonyResourceMap>>
|
|
566
|
+
= (config, action) => withHarmonyMod(config, ['colors', action]);
|
|
567
|
+
export const withMedia: ConfigPlugin<HarmonyModAction<HarmonyMediaMap>>
|
|
568
|
+
= (config, action) => withHarmonyMod(config, ['media', action]);
|
|
569
|
+
export const withProfiles: ConfigPlugin<HarmonyModAction<HarmonyJson>>
|
|
570
|
+
= (config, action) => withHarmonyMod(config, ['profiles', action]);
|
|
571
|
+
export const withEntryAbility: ConfigPlugin<HarmonyModAction<string>>
|
|
572
|
+
= (config, action) => withHarmonyMod(config, ['entryAbility', action]);
|
|
573
|
+
export const withIndexPage: ConfigPlugin<HarmonyModAction<string>>
|
|
574
|
+
= (config, action) => withHarmonyMod(config, ['indexPage', action]);
|
|
575
|
+
export const withWorker: ConfigPlugin<HarmonyModAction<string>>
|
|
576
|
+
= (config, action) => withHarmonyMod(config, ['worker', action]);
|
|
577
|
+
export const withArkTSPackageProvider: ConfigPlugin<HarmonyModAction<string>>
|
|
578
|
+
= (config, action) => withHarmonyMod(config, ['arktsPackageProvider', action]);
|
|
579
|
+
export const withCppPackageProvider: ConfigPlugin<HarmonyModAction<string>>
|
|
580
|
+
= (config, action) => withHarmonyMod(config, ['cppPackageProvider', action]);
|
|
581
|
+
export const withCMakeLists: ConfigPlugin<HarmonyModAction<string>>
|
|
582
|
+
= (config, action) => withHarmonyMod(config, ['cmakeLists', action]);
|
|
583
|
+
export const withHarmonyAutolinking: ConfigPlugin<HarmonyModAction<null>>
|
|
584
|
+
= (config, action) => withHarmonyMod(config, ['autolinking', action]);
|
|
585
|
+
export const withCngManifest: ConfigPlugin<HarmonyModAction<null>>
|
|
586
|
+
= (config, action) => withHarmonyMod(config, ['manifest', action]);
|
|
587
|
+
export const withHarmonyDangerousMod: ConfigPlugin<HarmonyModAction<null>>
|
|
588
|
+
= (config, action) => withHarmonyMod(config, ['dangerous', action]);
|
|
589
|
+
|
|
590
|
+
export const withHarmonyResources: ConfigPlugin<HarmonyModAction<unknown>> = (config, action) => {
|
|
591
|
+
for (const modName of ResourceModNames) {
|
|
592
|
+
withHarmonyMod(config, [modName, action]);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return config;
|
|
596
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import JSON5 from 'json5';
|
|
6
|
+
|
|
7
|
+
const HarmonyNativeInputsFingerprintVersion = 1;
|
|
8
|
+
|
|
9
|
+
interface HarmonyNativeInputsFingerprint {
|
|
10
|
+
artifactCount: number;
|
|
11
|
+
fingerprint: string;
|
|
12
|
+
fingerprintVersion: typeof HarmonyNativeInputsFingerprintVersion;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface HarmonyNativeInputsFingerprintOptions {
|
|
16
|
+
lockfile: string;
|
|
17
|
+
manifest: string;
|
|
18
|
+
projectRoot: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface PackageManifest {
|
|
22
|
+
dependencies?: Record<string, unknown>;
|
|
23
|
+
overrides?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readOptionalFile(file: string): Uint8Array | null {
|
|
27
|
+
try {
|
|
28
|
+
return Uint8Array.from(fs.readFileSync(file));
|
|
29
|
+
} catch (cause: unknown) {
|
|
30
|
+
if (cause && typeof cause === 'object' && 'code' in cause && cause.code === 'ENOENT') return null;
|
|
31
|
+
|
|
32
|
+
throw new Error(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveLocalHar(root: string, specifier: unknown): string | null {
|
|
37
|
+
if (typeof specifier !== 'string' || !/^\.\.?[\\/]/u.test(specifier)) return null;
|
|
38
|
+
if (!specifier.toLowerCase().endsWith('.har')) return null;
|
|
39
|
+
|
|
40
|
+
return path.resolve(root, specifier);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function findLocalHars(root: string, manifest: PackageManifest): string[] {
|
|
44
|
+
const specifiers = [
|
|
45
|
+
...Object.values(manifest.dependencies ?? {}),
|
|
46
|
+
...Object.values(manifest.overrides ?? {}),
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
return [...new Set(specifiers.map(specifier => resolveLocalHar(root, specifier)))]
|
|
50
|
+
.filter((value): value is string => value !== null)
|
|
51
|
+
.sort();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hashFile(hash: crypto.Hash, file: string): void {
|
|
55
|
+
const handle = fs.openSync(file, 'r');
|
|
56
|
+
const buffer = new Uint8Array(1024 * 1024);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
while (true) {
|
|
60
|
+
const size = fs.readSync(handle, buffer, 0, buffer.length, null);
|
|
61
|
+
if (size === 0) break;
|
|
62
|
+
|
|
63
|
+
hash.update(Uint8Array.from(buffer.subarray(0, size)));
|
|
64
|
+
}
|
|
65
|
+
} finally {
|
|
66
|
+
fs.closeSync(handle);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function fingerprintHarmonyNativeInputsSync(
|
|
71
|
+
options: HarmonyNativeInputsFingerprintOptions
|
|
72
|
+
): HarmonyNativeInputsFingerprint {
|
|
73
|
+
const data = fs.readFileSync(options.manifest);
|
|
74
|
+
const lock = readOptionalFile(options.lockfile);
|
|
75
|
+
|
|
76
|
+
const manifest = JSON5.parse(data.toString('utf8')) as PackageManifest;
|
|
77
|
+
const root = path.dirname(options.manifest);
|
|
78
|
+
const files = findLocalHars(root, manifest);
|
|
79
|
+
|
|
80
|
+
const hash = crypto.createHash('sha256');
|
|
81
|
+
|
|
82
|
+
hash.update(`expo-harmony-native-dependencies-v${HarmonyNativeInputsFingerprintVersion}\0`);
|
|
83
|
+
hash.update(Uint8Array.from(data));
|
|
84
|
+
if (lock) hash.update(lock);
|
|
85
|
+
|
|
86
|
+
for (const file of files) {
|
|
87
|
+
if (!fs.statSync(file).isFile()) {
|
|
88
|
+
throw new Error(`Cannot fingerprint Harmony native artifact: ${file}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const relative = path.relative(options.projectRoot, file).split(path.sep).join('/');
|
|
92
|
+
|
|
93
|
+
hash.update('\0artifact\0');
|
|
94
|
+
hash.update(relative);
|
|
95
|
+
hash.update('\0');
|
|
96
|
+
hashFile(hash, file);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
artifactCount: files.length,
|
|
101
|
+
fingerprint: hash.digest('hex'),
|
|
102
|
+
fingerprintVersion: HarmonyNativeInputsFingerprintVersion,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export {
|
|
107
|
+
HarmonyNativeInputsFingerprintVersion,
|
|
108
|
+
fingerprintHarmonyNativeInputsSync,
|
|
109
|
+
};
|
|
110
|
+
export type {
|
|
111
|
+
HarmonyNativeInputsFingerprint,
|
|
112
|
+
HarmonyNativeInputsFingerprintOptions,
|
|
113
|
+
};
|