@expo-harmony/config-plugins 55.0.10-harmony.2 → 55.0.10-harmony.4

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.
Files changed (50) hide show
  1. package/README.md +203 -11
  2. package/build/compiler.d.ts +7 -0
  3. package/build/compiler.js +12 -0
  4. package/build/composition.d.ts +24 -0
  5. package/build/composition.js +11 -0
  6. package/build/config.d.ts +2 -15
  7. package/build/customFiles.d.ts +15 -0
  8. package/build/customFiles.js +108 -0
  9. package/build/files.d.ts +1 -1
  10. package/build/files.js +2 -2
  11. package/build/generatedFiles.d.ts +15 -0
  12. package/build/generatedFiles.js +70 -0
  13. package/build/index.d.ts +20 -5
  14. package/build/index.js +53 -6
  15. package/build/internal.d.ts +3 -0
  16. package/build/internal.js +13 -0
  17. package/build/manifest.d.ts +38 -0
  18. package/build/manifest.js +71 -0
  19. package/build/mods.d.ts +37 -31
  20. package/build/mods.js +118 -86
  21. package/build/normalizeConfig.d.ts +10 -3
  22. package/build/normalizeConfig.js +45 -47
  23. package/build/ownership.d.ts +4 -3
  24. package/build/ownership.js +17 -0
  25. package/build/paths.d.ts +13 -10
  26. package/build/paths.js +32 -31
  27. package/build/permissions.d.ts +12 -0
  28. package/build/permissions.js +60 -0
  29. package/build/pluginTypes.d.ts +15 -0
  30. package/build/pluginTypes.js +2 -0
  31. package/build/resources.d.ts +13 -0
  32. package/build/resources.js +55 -0
  33. package/package.json +9 -9
  34. package/src/compiler.ts +21 -0
  35. package/src/composition.ts +45 -0
  36. package/src/config.ts +3 -19
  37. package/src/customFiles.ts +142 -0
  38. package/src/files.ts +3 -4
  39. package/src/generatedFiles.ts +85 -0
  40. package/src/index.ts +20 -11
  41. package/src/internal.ts +3 -0
  42. package/src/manifest.ts +151 -0
  43. package/src/mods.ts +180 -128
  44. package/src/nativeInputs.ts +0 -1
  45. package/src/normalizeConfig.ts +56 -59
  46. package/src/ownership.ts +25 -5
  47. package/src/paths.ts +41 -45
  48. package/src/permissions.ts +68 -0
  49. package/src/pluginTypes.ts +24 -0
  50. package/src/resources.ts +65 -0
package/src/mods.ts CHANGED
@@ -3,11 +3,15 @@ import path from 'node:path';
3
3
 
4
4
  import {
5
5
  withBaseMod, withMod,
6
- type ConfigPlugin, type ExportedConfig, type ExportedConfigWithProps, type ModPlatform,
6
+ type ExportedConfig, type ModPlatform,
7
7
  } from '@expo/config-plugins';
8
8
  import type { ExpoConfig } from '@expo/config-types';
9
9
 
10
10
  import { HarmonyConfigPluginError } from './errors';
11
+ import type { ExpoConfigWithHarmony } from './config';
12
+ import type { HarmonyModuleJson } from './manifest';
13
+ import type { HarmonyConfigPlugin, HarmonyModAction, HarmonyModConfig } from './pluginTypes';
14
+ import type { HarmonyResourceFile } from './resources';
11
15
  import { atomicWrite, readJson5, readText, writeJson5 } from './files';
12
16
  import {
13
17
  ManagedPaths, ProjectPaths, ResourcePaths,
@@ -21,7 +25,6 @@ export type HarmonyModName
21
25
  | 'projectBuildProfile'
22
26
  | 'rootOhPackage'
23
27
  | 'rootHvigor'
24
- | 'nativeInputsStamp'
25
28
  | 'hvigorConfig'
26
29
  | 'entryBuildProfile'
27
30
  | 'entryOhPackage'
@@ -31,21 +34,20 @@ export type HarmonyModName
31
34
  | 'colors'
32
35
  | 'media'
33
36
  | 'profiles'
37
+ | 'abilityStage'
34
38
  | 'entryAbility'
35
39
  | 'indexPage'
36
40
  | 'worker'
37
41
  | 'arktsPackageProvider'
38
42
  | 'cppPackageProvider'
39
43
  | 'cmakeLists'
40
- | 'autolinking'
41
- | 'manifest';
44
+ | 'autolinking';
42
45
 
43
- export type HarmonyModAction<T = unknown> = (
44
- config: ExportedConfigWithProps<T>
45
- ) => ExportedConfigWithProps<T> | Promise<ExportedConfigWithProps<T>>;
46
+ type InternalModName = 'nativeInputsStamp' | 'manifest' | 'prepare' | 'patch';
47
+ type AnyModName = HarmonyModName | InternalModName;
46
48
 
47
49
  export type HarmonyJson = Record<string, unknown>;
48
- export type HarmonyResourceMap = Record<string, HarmonyJson>;
50
+ export type HarmonyResourceMap = Record<string, HarmonyResourceFile>;
49
51
  export type HarmonyMediaDescriptor
50
52
  = | {
51
53
  source: string;
@@ -96,6 +98,7 @@ const TextMods = new Set<FileModName>([
96
98
  'rootHvigor',
97
99
  'nativeInputsStamp',
98
100
  'entryHvigor',
101
+ 'abilityStage',
99
102
  'entryAbility',
100
103
  'indexPage',
101
104
  'worker',
@@ -104,11 +107,12 @@ const TextMods = new Set<FileModName>([
104
107
  'cmakeLists',
105
108
  ]);
106
109
  const ResourceMods = new Set<HarmonyModName>(['strings', 'colors', 'media']);
107
- const VirtualMods = new Set<HarmonyModName>(['dangerous', 'autolinking', 'manifest']);
110
+ const VirtualMods = new Set<AnyModName>(['dangerous', 'autolinking', 'manifest', 'prepare', 'patch']);
108
111
  const ResourceModNames: readonly ResourceModName[] = ['strings', 'colors', 'media'];
109
112
 
110
- export const HarmonyModNames: readonly HarmonyModName[] = Object.freeze([
113
+ const AllModNames: readonly AnyModName[] = Object.freeze([
111
114
  'dangerous',
115
+ 'prepare',
112
116
  'reactNativeConfig',
113
117
  'appJson',
114
118
  'projectBuildProfile',
@@ -124,6 +128,7 @@ export const HarmonyModNames: readonly HarmonyModName[] = Object.freeze([
124
128
  'colors',
125
129
  'media',
126
130
  'profiles',
131
+ 'abilityStage',
127
132
  'entryAbility',
128
133
  'indexPage',
129
134
  'worker',
@@ -131,9 +136,14 @@ export const HarmonyModNames: readonly HarmonyModName[] = Object.freeze([
131
136
  'cppPackageProvider',
132
137
  'cmakeLists',
133
138
  'autolinking',
139
+ 'patch',
134
140
  'manifest',
135
141
  ]);
136
142
 
143
+ export const HarmonyModNames: readonly HarmonyModName[] = Object.freeze(
144
+ AllModNames.filter((name): name is HarmonyModName => name !== 'manifest' && name !== 'nativeInputsStamp' && name !== 'prepare' && name !== 'patch')
145
+ );
146
+
137
147
  export function recordManagedFile(config: ManagedConfig, file: string, owner: string): void {
138
148
  config._internal ??= {};
139
149
  config._internal.harmonyManagedFiles ??= [];
@@ -146,7 +156,7 @@ export function recordManagedFile(config: ManagedConfig, file: string, owner: st
146
156
  config._internal.harmonyManagedFiles = files;
147
157
  }
148
158
 
149
- function assertModResults(modName: HarmonyModName, value: unknown, kind: 'json' | 'resource' | 'text'): void {
159
+ function assertModResults(name: AnyModName, value: unknown, kind: 'json' | 'resource' | 'text'): void {
150
160
  const valid = kind === 'text'
151
161
  ? typeof value === 'string'
152
162
  : value !== null && typeof value === 'object' && !Array.isArray(value);
@@ -154,35 +164,42 @@ function assertModResults(modName: HarmonyModName, value: unknown, kind: 'json'
154
164
  if (!valid) {
155
165
  throw new HarmonyConfigPluginError(
156
166
  'ERR_HARMONY_MOD_RESULTS_INVALID',
157
- `harmony.${modName} must return ${kind === 'text' ? 'a string' : 'an object'} modResults.`,
158
- { operation: `harmony.${modName}.write` }
167
+ `harmony.${name} must return ${kind === 'text' ? 'a string' : 'an object'} modResults.`,
168
+ { operation: `harmony.${name}.write` }
159
169
  );
160
170
  }
161
171
  }
162
172
 
163
- function withFileProvider(config: ExpoConfig, modName: FileModName): ExpoConfig {
173
+ function withFileProvider(config: ExpoConfig, name: FileModName): ExpoConfig {
164
174
  return withBaseMod<FileResult>(config, {
165
175
  platform: 'harmony' as ModPlatform,
166
- mod: modName,
176
+ mod: name,
167
177
  isProvider: true,
168
- isIntrospective: JsonMods.has(modName),
169
- saveToInternal: JsonMods.has(modName),
178
+ isIntrospective: JsonMods.has(name),
179
+ saveToInternal: JsonMods.has(name),
170
180
  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');
181
+ const { nextMod: next, ...request } = value.modRequest;
182
+ const project = name in ProjectPaths;
183
+ const root = project ? request.projectRoot : request.platformProjectRoot;
184
+ const file = project
185
+ ? await resolveProjectPath(root, name as keyof typeof ProjectPaths)
186
+ : await resolveHarmonyPath(root, ManagedPaths[name as keyof typeof ManagedPaths]);
187
+ const exists = !request.ignoreExistingNativeFiles && fs.existsSync(file);
188
+ const template = value._internal?.harmonyTemplateDirectory;
189
+ const input = !exists && !project && typeof template === 'string'
190
+ ? await resolveHarmonyPath(template, ManagedPaths[name as keyof typeof ManagedPaths])
191
+ : file;
192
+
193
+ const data = request.ignoreExistingNativeFiles && input === file
194
+ ? (JsonMods.has(name) ? {} : '')
195
+ : JsonMods.has(name)
196
+ ? await readJson5<HarmonyJson>(input, {}, name)
197
+ : await readText(file);
198
+
199
+ const context = { ...request, modFile: file, modFileExists: exists } as typeof value.modRequest;
200
+ const result = await next!({ ...value, modRequest: context, modResults: data });
201
+
202
+ assertModResults(name, result.modResults, JsonMods.has(name) ? 'json' : 'text');
186
203
 
187
204
  if (!result.modRequest.introspect) {
188
205
  if (typeof result.modResults === 'string') {
@@ -191,7 +208,7 @@ function withFileProvider(config: ExpoConfig, modName: FileModName): ExpoConfig
191
208
  await writeJson5(file, result.modResults);
192
209
  }
193
210
 
194
- recordManagedFile(result, file, modName);
211
+ recordManagedFile(result, file, name);
195
212
  }
196
213
 
197
214
  return result;
@@ -202,13 +219,18 @@ function withFileProvider(config: ExpoConfig, modName: FileModName): ExpoConfig
202
219
  async function readResourceMap(
203
220
  root: string,
204
221
  entries: Readonly<Record<string, string>>,
205
- modName: ResourceModName
222
+ name: ResourceModName,
223
+ template?: string,
224
+ ignore = false
206
225
  ): Promise<HarmonyResourceMap> {
207
226
  const resources: HarmonyResourceMap = {};
208
227
 
209
228
  for (const [scope, relative] of Object.entries(entries)) {
210
229
  const file = await resolveHarmonyPath(root, relative);
211
- resources[scope] = await readJson5<HarmonyJson>(file, {}, modName);
230
+ const fallback = ignore || !fs.existsSync(file);
231
+ const input = fallback && template ? await resolveHarmonyPath(template, relative) : file;
232
+
233
+ resources[scope] = ignore && input === file ? {} : await readJson5<HarmonyJson>(input, {}, name);
212
234
  }
213
235
 
214
236
  return resources;
@@ -221,6 +243,7 @@ async function readMediaMap(root: string): Promise<HarmonyMediaMap> {
221
243
  const directory = await resolveHarmonyPath(root, relative);
222
244
 
223
245
  media[scope] = {};
246
+
224
247
  let names: string[] = [];
225
248
 
226
249
  try {
@@ -274,6 +297,7 @@ async function writeMediaMap(
274
297
  { operation: 'harmony.media.write' }
275
298
  );
276
299
  }
300
+
277
301
  if (!descriptor || typeof descriptor !== 'object' || Array.isArray(descriptor)) {
278
302
  throw new HarmonyConfigPluginError(
279
303
  'ERR_HARMONY_CONFIG_INVALID',
@@ -283,16 +307,14 @@ async function writeMediaMap(
283
307
  }
284
308
 
285
309
  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) {
310
+ if (Object.hasOwn(item, 'source') === Object.hasOwn(item, 'content')) {
290
311
  throw new HarmonyConfigPluginError(
291
312
  'ERR_HARMONY_CONFIG_INVALID',
292
313
  `Harmony media ${scope}/${name} must define exactly one of source or content.`,
293
314
  { operation: 'harmony.media.write' }
294
315
  );
295
316
  }
317
+
296
318
  if (item.replaceBase !== undefined && typeof item.replaceBase !== 'boolean') {
297
319
  throw new HarmonyConfigPluginError(
298
320
  'ERR_HARMONY_CONFIG_INVALID',
@@ -303,7 +325,7 @@ async function writeMediaMap(
303
325
 
304
326
  let content: string | Uint8Array;
305
327
 
306
- if (hasSource) {
328
+ if (Object.hasOwn(item, 'source')) {
307
329
  if (typeof item.source !== 'string' || item.source.length === 0) {
308
330
  throw new HarmonyConfigPluginError(
309
331
  'ERR_HARMONY_CONFIG_INVALID',
@@ -334,11 +356,9 @@ async function writeMediaMap(
334
356
  }
335
357
  }
336
358
 
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
359
  for (const [scope, names] of Object.entries(previous)) {
341
360
  if (!(scope in ResourcePaths.media)) continue;
361
+
342
362
  const directory = await resolveHarmonyPath(
343
363
  root,
344
364
  ResourcePaths.media[scope as keyof typeof ResourcePaths.media]
@@ -360,9 +380,9 @@ async function writeMediaMap(
360
380
  if (replaceBase) {
361
381
  const base = path.parse(name).name;
362
382
 
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 });
383
+ for (const entry of await fs.promises.readdir(directory)) {
384
+ if (path.parse(entry).name === base && entry !== name) {
385
+ await fs.promises.rm(path.join(directory, entry), { force: true });
366
386
  }
367
387
  }
368
388
  }
@@ -374,53 +394,56 @@ async function writeMediaMap(
374
394
  }
375
395
  }
376
396
 
377
- function withResourceProvider(config: ExpoConfig, modName: ResourceModName): ExpoConfig {
378
- const isIntrospective = modName !== 'media';
397
+ function withResourceProvider(config: ExpoConfig, name: ResourceModName): ExpoConfig {
398
+ const introspective = name !== 'media';
379
399
 
380
400
  return withBaseMod<ResourceResult>(config, {
381
401
  platform: 'harmony' as ModPlatform,
382
- mod: modName,
402
+ mod: name,
383
403
  isProvider: true,
384
- isIntrospective,
385
- saveToInternal: isIntrospective,
404
+ isIntrospective: introspective,
405
+ saveToInternal: introspective,
386
406
  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'
407
+ const { nextMod: next, ...request } = value.modRequest;
408
+ const root = request.platformProjectRoot;
409
+ const data = name === 'media'
410
+ ? (request.ignoreExistingNativeFiles
411
+ ? Object.fromEntries(Object.keys(ResourcePaths.media).map(scope => [scope, {}]))
412
+ : await readMediaMap(root))
413
+ : await readResourceMap(
414
+ root, ResourcePaths[name], name,
415
+ value._internal?.harmonyTemplateDirectory, request.ignoreExistingNativeFiles
416
+ );
417
+ const previous = name === 'media'
393
418
  ? Object.fromEntries(
394
419
  Object.entries(data).map(([scope, files]) => [scope, Object.keys(files)])
395
420
  )
396
421
  : {};
397
422
 
398
- const result = await nextMod!({ ...value, modRequest, modResults: data });
423
+ const result = await next!({ ...value, modRequest: request, modResults: data });
399
424
 
400
- assertModResults(modName, result.modResults, 'resource');
425
+ assertModResults(name, result.modResults, 'resource');
401
426
 
402
427
  if (!result.modRequest.introspect) {
403
- if (modName === 'media') {
428
+ if (name === 'media') {
404
429
  await writeMediaMap(root, result.modResults as HarmonyMediaMap, result, previous);
405
430
  } else {
406
- const paths = ResourcePaths[modName];
431
+ const paths = ResourcePaths[name];
407
432
 
408
433
  for (const [scope, resource] of Object.entries(result.modResults)) {
409
434
  if (!(scope in paths)) continue;
410
435
 
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
436
  if (!resource || typeof resource !== 'object' || Array.isArray(resource)) {
415
437
  throw new HarmonyConfigPluginError(
416
438
  'ERR_HARMONY_CONFIG_INVALID',
417
- `Harmony ${modName} scope ${scope} must be an object.`,
418
- { operation: `harmony.${modName}.write` }
439
+ `Harmony ${name} scope ${scope} must be an object.`,
440
+ { operation: `harmony.${name}.write` }
419
441
  );
420
442
  }
421
443
 
422
444
  const file = await resolveHarmonyPath(root, paths[scope as keyof typeof paths]);
423
445
 
446
+ // Harmony's resource compiler rejects an empty JSON root.
424
447
  if (Object.keys(resource).length === 0) {
425
448
  await fs.promises.rm(file, { force: true });
426
449
 
@@ -428,7 +451,7 @@ function withResourceProvider(config: ExpoConfig, modName: ResourceModName): Exp
428
451
  }
429
452
 
430
453
  await writeJson5(file, resource);
431
- recordManagedFile(result, file, modName);
454
+ recordManagedFile(result, file, name);
432
455
  }
433
456
  }
434
457
  }
@@ -438,158 +461,187 @@ function withResourceProvider(config: ExpoConfig, modName: ResourceModName): Exp
438
461
  });
439
462
  }
440
463
 
441
- function withVirtualProvider(config: ExpoConfig, modName: HarmonyModName): ExpoConfig {
464
+ function withVirtualProvider(config: ExpoConfig, name: AnyModName): ExpoConfig {
442
465
  return withBaseMod<null>(config, {
443
466
  platform: 'harmony' as ModPlatform,
444
- mod: modName,
467
+ mod: name,
445
468
  isProvider: true,
469
+ isIntrospective: name === 'prepare',
446
470
  async action(value) {
447
- const { nextMod, ...modRequest } = value.modRequest;
448
-
449
- return nextMod!({ ...value, modRequest, modResults: null });
471
+ const { nextMod: next, ...request } = value.modRequest;
472
+ return next!({ ...value, modRequest: request, modResults: null });
450
473
  },
451
474
  });
452
475
  }
453
476
 
454
- function withProvider(config: ExpoConfig, modName: HarmonyModName): ExpoConfig {
477
+ function withProvider(config: ExpoConfig, name: AnyModName): ExpoConfig {
455
478
  const actions: HarmonyModAction[] = [];
456
479
 
457
480
  config = withMod(config, {
458
481
  platform: 'harmony' as ModPlatform,
459
- mod: modName,
482
+ mod: name,
460
483
  async action(value) {
461
484
  let result = value;
462
485
 
463
- // withMod is a stack: the last registered plugin runs first. Preserve
464
- // that ordering for Harmony plugins registered after the provider.
486
+ // Late mods must follow Expo's last-registered-first order.
465
487
  for (let index = actions.length - 1; index >= 0; index -= 1) {
466
- result = await actions[index](result);
488
+ result = await actions[index](result as unknown as HarmonyModConfig<unknown>) as unknown as typeof result;
467
489
  }
468
490
 
469
491
  return result;
470
492
  },
471
493
  });
472
494
 
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);
495
+ if (JsonMods.has(name as FileModName) || TextMods.has(name as FileModName)) {
496
+ config = withFileProvider(config, name as FileModName);
497
+ } else if (ResourceMods.has(name as HarmonyModName)) {
498
+ config = withResourceProvider(config, name as ResourceModName);
499
+ } else if (VirtualMods.has(name)) {
500
+ config = withVirtualProvider(config, name);
479
501
  } else {
480
502
  throw new HarmonyConfigPluginError(
481
503
  'ERR_HARMONY_MOD_NOT_REGISTERED',
482
- `Unknown Harmony mod: ${modName}`,
504
+ `Unknown Harmony mod: ${name}`,
483
505
  { operation: 'register-base-mods' }
484
506
  );
485
507
  }
486
508
 
487
509
  const mods = (config as ExportedConfig).mods as unknown as ModTable | undefined;
488
- const provider = mods?.harmony?.[modName];
510
+ const provider = mods?.harmony?.[name];
489
511
 
490
512
  if (provider?.isProvider) provider.expoHarmonyLateActions = actions;
491
513
 
492
514
  return config;
493
515
  }
494
516
 
495
- export function withHarmonyBaseMods(config: ExpoConfig): ExpoConfig {
496
- for (const modName of HarmonyModNames) {
517
+ export function withHarmonyBaseMods<Config extends ExpoConfigWithHarmony>(config: Config): Config {
518
+ for (const name of AllModNames) {
497
519
  const mods = (config as ExportedConfig).mods as unknown as ModTable;
498
520
 
499
- if (!mods?.harmony?.[modName]?.isProvider) config = withProvider(config, modName);
521
+ if (!mods?.harmony?.[name]?.isProvider) config = withProvider(config as ExpoConfig, name) as Config;
500
522
  }
501
523
 
502
524
  const mods = (config as ExportedConfig).mods as unknown as ModTable;
503
525
  const table = mods.harmony!;
504
526
 
505
527
  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)),
528
+ ...AllModNames.filter(name => table[name] && name !== 'manifest' && name !== 'patch').map(name => [name, table[name]]),
529
+ ...Object.entries(table).filter(([name]) => !AllModNames.includes(name as AnyModName)),
530
+ ['patch', table.patch],
531
+ ['manifest', table.manifest],
508
532
  ]);
509
533
 
510
534
  return config;
511
535
  }
512
536
 
513
- export function withHarmonyMod<T = unknown>(
514
- config: ExpoConfig,
537
+ export function withHarmonyMod<T = unknown, Config extends ExpoConfigWithHarmony = ExpoConfigWithHarmony>(
538
+ config: Config,
515
539
  tuple: [HarmonyModName, HarmonyModAction<T>]
516
- ): ExpoConfig {
540
+ ): Config {
517
541
  if (!Array.isArray(tuple) || tuple.length !== 2) {
518
542
  throw new TypeError('withHarmonyMod expects [modName, action].');
519
543
  }
520
544
 
521
- const [modName, action] = tuple;
522
- if (!HarmonyModNames.includes(modName) || typeof action !== 'function') {
545
+ const [name, action] = tuple;
546
+ if (!HarmonyModNames.includes(name) || typeof action !== 'function') {
523
547
  throw new HarmonyConfigPluginError(
524
548
  'ERR_HARMONY_MOD_NOT_REGISTERED',
525
- `Cannot register Harmony mod: ${modName}`,
549
+ `Cannot register Harmony mod: ${name}`,
526
550
  { operation: 'register-mod' }
527
551
  );
528
552
  }
529
553
 
554
+ return registerMod(config, name, action);
555
+ }
556
+
557
+ export function registerMod<T, Config extends ExpoConfigWithHarmony>(
558
+ config: Config,
559
+ name: string,
560
+ action: HarmonyModAction<T>
561
+ ): Config {
530
562
  const mods = (config as ExportedConfig).mods as unknown as ModTable | undefined;
531
- const provider = mods?.harmony?.[modName];
563
+ const provider = mods?.harmony?.[name];
532
564
 
533
565
  if (provider?.isProvider && provider.expoHarmonyLateActions) {
534
- provider.expoHarmonyLateActions.push(action as HarmonyModAction);
566
+ // Build an ordinary Expo interceptor on an isolated mod table, preserving
567
+ // its registration stack, error behavior and execution trace for late mods.
568
+ const carrier = withMod<T>({ ...config, mods: {} } as ExpoConfig, {
569
+ platform: 'harmony' as ModPlatform,
570
+ mod: name,
571
+ action: action as unknown as Parameters<typeof withMod<T>>[1]['action'],
572
+ });
573
+ const interceptor = (carrier.mods as unknown as Record<string, Record<string, HarmonyModAction>>).harmony[name];
574
+
575
+ provider.expoHarmonyLateActions.push(interceptor);
576
+
535
577
  return config;
536
578
  }
537
579
 
538
- return withMod<T>(config, { platform: 'harmony' as ModPlatform, mod: modName, action });
580
+ return withMod<T>(config as ExpoConfig, {
581
+ platform: 'harmony' as ModPlatform,
582
+ mod: name,
583
+ action: action as unknown as Parameters<typeof withMod<T>>[1]['action'],
584
+ }) as Config;
539
585
  }
540
586
 
541
- export const withReactNativeConfig: ConfigPlugin<HarmonyModAction<string>>
587
+ export const withReactNativeConfig: HarmonyConfigPlugin<HarmonyModAction<string>>
542
588
  = (config, action) => withHarmonyMod(config, ['reactNativeConfig', action]);
543
- export const withAppJson: ConfigPlugin<HarmonyModAction<HarmonyJson>>
589
+ export const withAppJson: HarmonyConfigPlugin<HarmonyModAction<HarmonyJson>>
544
590
  = (config, action) => withHarmonyMod(config, ['appJson', action]);
545
- export const withProjectBuildProfile: ConfigPlugin<HarmonyModAction<HarmonyJson>>
591
+ export const withProjectBuildProfile: HarmonyConfigPlugin<HarmonyModAction<HarmonyJson>>
546
592
  = (config, action) => withHarmonyMod(config, ['projectBuildProfile', action]);
547
- export const withRootOhPackage: ConfigPlugin<HarmonyModAction<HarmonyJson>>
593
+ export const withRootOhPackage: HarmonyConfigPlugin<HarmonyModAction<HarmonyJson>>
548
594
  = (config, action) => withHarmonyMod(config, ['rootOhPackage', action]);
549
- export const withRootHvigor: ConfigPlugin<HarmonyModAction<string>>
595
+ export const withRootHvigor: HarmonyConfigPlugin<HarmonyModAction<string>>
550
596
  = (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>>
597
+ export const withNativeInputsStamp: HarmonyConfigPlugin<HarmonyModAction<string>>
598
+ = (config, action) => registerMod(config, 'nativeInputsStamp', action);
599
+ export const withHvigorConfig: HarmonyConfigPlugin<HarmonyModAction<HarmonyJson>>
554
600
  = (config, action) => withHarmonyMod(config, ['hvigorConfig', action]);
555
- export const withEntryBuildProfile: ConfigPlugin<HarmonyModAction<HarmonyJson>>
601
+ export const withEntryBuildProfile: HarmonyConfigPlugin<HarmonyModAction<HarmonyJson>>
556
602
  = (config, action) => withHarmonyMod(config, ['entryBuildProfile', action]);
557
- export const withEntryOhPackage: ConfigPlugin<HarmonyModAction<HarmonyJson>>
603
+ export const withEntryOhPackage: HarmonyConfigPlugin<HarmonyModAction<HarmonyJson>>
558
604
  = (config, action) => withHarmonyMod(config, ['entryOhPackage', action]);
559
- export const withEntryHvigor: ConfigPlugin<HarmonyModAction<string>>
605
+ export const withEntryHvigor: HarmonyConfigPlugin<HarmonyModAction<string>>
560
606
  = (config, action) => withHarmonyMod(config, ['entryHvigor', action]);
561
- export const withModuleJson: ConfigPlugin<HarmonyModAction<HarmonyJson>>
607
+ export const withModuleJson: HarmonyConfigPlugin<HarmonyModAction<HarmonyModuleJson>>
562
608
  = (config, action) => withHarmonyMod(config, ['moduleJson', action]);
563
- export const withStrings: ConfigPlugin<HarmonyModAction<HarmonyResourceMap>>
609
+ export const withStrings: HarmonyConfigPlugin<HarmonyModAction<HarmonyResourceMap>>
564
610
  = (config, action) => withHarmonyMod(config, ['strings', action]);
565
- export const withColors: ConfigPlugin<HarmonyModAction<HarmonyResourceMap>>
611
+ export const withColors: HarmonyConfigPlugin<HarmonyModAction<HarmonyResourceMap>>
566
612
  = (config, action) => withHarmonyMod(config, ['colors', action]);
567
- export const withMedia: ConfigPlugin<HarmonyModAction<HarmonyMediaMap>>
613
+ export const withMedia: HarmonyConfigPlugin<HarmonyModAction<HarmonyMediaMap>>
568
614
  = (config, action) => withHarmonyMod(config, ['media', action]);
569
- export const withProfiles: ConfigPlugin<HarmonyModAction<HarmonyJson>>
615
+ export const withProfiles: HarmonyConfigPlugin<HarmonyModAction<HarmonyJson>>
570
616
  = (config, action) => withHarmonyMod(config, ['profiles', action]);
571
- export const withEntryAbility: ConfigPlugin<HarmonyModAction<string>>
617
+ export const withAbilityStage: HarmonyConfigPlugin<HarmonyModAction<string>>
618
+ = (config, action) => withHarmonyMod(config, ['abilityStage', action]);
619
+ export const withEntryAbility: HarmonyConfigPlugin<HarmonyModAction<string>>
572
620
  = (config, action) => withHarmonyMod(config, ['entryAbility', action]);
573
- export const withIndexPage: ConfigPlugin<HarmonyModAction<string>>
621
+ export const withIndexPage: HarmonyConfigPlugin<HarmonyModAction<string>>
574
622
  = (config, action) => withHarmonyMod(config, ['indexPage', action]);
575
- export const withWorker: ConfigPlugin<HarmonyModAction<string>>
623
+ export const withWorker: HarmonyConfigPlugin<HarmonyModAction<string>>
576
624
  = (config, action) => withHarmonyMod(config, ['worker', action]);
577
- export const withArkTSPackageProvider: ConfigPlugin<HarmonyModAction<string>>
625
+ export const withArkTSPackageProvider: HarmonyConfigPlugin<HarmonyModAction<string>>
578
626
  = (config, action) => withHarmonyMod(config, ['arktsPackageProvider', action]);
579
- export const withCppPackageProvider: ConfigPlugin<HarmonyModAction<string>>
627
+ export const withCppPackageProvider: HarmonyConfigPlugin<HarmonyModAction<string>>
580
628
  = (config, action) => withHarmonyMod(config, ['cppPackageProvider', action]);
581
- export const withCMakeLists: ConfigPlugin<HarmonyModAction<string>>
629
+ export const withCMakeLists: HarmonyConfigPlugin<HarmonyModAction<string>>
582
630
  = (config, action) => withHarmonyMod(config, ['cmakeLists', action]);
583
- export const withHarmonyAutolinking: ConfigPlugin<HarmonyModAction<null>>
631
+ export const withHarmonyAutolinking: HarmonyConfigPlugin<HarmonyModAction<null>>
584
632
  = (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>>
633
+ export const withCngManifest: HarmonyConfigPlugin<HarmonyModAction<null>>
634
+ = (config, action) => registerMod(config, 'manifest', action);
635
+ export const withPreparation: HarmonyConfigPlugin<HarmonyModAction<null>>
636
+ = (config, action) => registerMod(config, 'prepare', action);
637
+ export const withProjectPatch: HarmonyConfigPlugin<HarmonyModAction<null>>
638
+ = (config, action) => registerMod(config, 'patch', action);
639
+ export const withHarmonyDangerousMod: HarmonyConfigPlugin<HarmonyModAction<null>>
588
640
  = (config, action) => withHarmonyMod(config, ['dangerous', action]);
589
641
 
590
- export const withHarmonyResources: ConfigPlugin<HarmonyModAction<unknown>> = (config, action) => {
591
- for (const modName of ResourceModNames) {
592
- withHarmonyMod(config, [modName, action]);
642
+ export const withHarmonyResources: HarmonyConfigPlugin<HarmonyModAction<unknown>> = (config, action) => {
643
+ for (const name of ResourceModNames) {
644
+ withHarmonyMod(config, [name, action]);
593
645
  }
594
646
 
595
647
  return config;
@@ -78,7 +78,6 @@ function fingerprintHarmonyNativeInputsSync(
78
78
  const files = findLocalHars(root, manifest);
79
79
 
80
80
  const hash = crypto.createHash('sha256');
81
-
82
81
  hash.update(`expo-harmony-native-dependencies-v${HarmonyNativeInputsFingerprintVersion}\0`);
83
82
  hash.update(Uint8Array.from(data));
84
83
  if (lock) hash.update(lock);