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

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