@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/build/mods.js CHANGED
@@ -3,10 +3,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.withHarmonyResources = exports.withHarmonyDangerousMod = exports.withCngManifest = exports.withHarmonyAutolinking = exports.withCMakeLists = exports.withCppPackageProvider = exports.withArkTSPackageProvider = exports.withWorker = exports.withIndexPage = exports.withEntryAbility = exports.withProfiles = exports.withMedia = exports.withColors = exports.withStrings = exports.withModuleJson = exports.withEntryHvigor = exports.withEntryOhPackage = exports.withEntryBuildProfile = exports.withHvigorConfig = exports.withNativeInputsStamp = exports.withRootHvigor = exports.withRootOhPackage = exports.withProjectBuildProfile = exports.withAppJson = exports.withReactNativeConfig = exports.HarmonyModNames = void 0;
6
+ exports.withHarmonyResources = exports.withHarmonyDangerousMod = exports.withProjectPatch = exports.withPreparation = exports.withCngManifest = exports.withHarmonyAutolinking = exports.withCMakeLists = exports.withCppPackageProvider = exports.withArkTSPackageProvider = exports.withWorker = exports.withIndexPage = exports.withEntryAbility = exports.withProfiles = exports.withMedia = exports.withColors = exports.withStrings = exports.withModuleJson = exports.withEntryHvigor = exports.withEntryOhPackage = exports.withEntryBuildProfile = exports.withHvigorConfig = exports.withNativeInputsStamp = exports.withRootHvigor = exports.withRootOhPackage = exports.withProjectBuildProfile = exports.withAppJson = exports.withReactNativeConfig = exports.HarmonyModNames = void 0;
7
7
  exports.recordManagedFile = recordManagedFile;
8
8
  exports.withHarmonyBaseMods = withHarmonyBaseMods;
9
9
  exports.withHarmonyMod = withHarmonyMod;
10
+ exports.registerMod = registerMod;
10
11
  const node_fs_1 = __importDefault(require("node:fs"));
11
12
  const node_path_1 = __importDefault(require("node:path"));
12
13
  const config_plugins_1 = require("@expo/config-plugins");
@@ -36,10 +37,11 @@ const TextMods = new Set([
36
37
  'cmakeLists',
37
38
  ]);
38
39
  const ResourceMods = new Set(['strings', 'colors', 'media']);
39
- const VirtualMods = new Set(['dangerous', 'autolinking', 'manifest']);
40
+ const VirtualMods = new Set(['dangerous', 'autolinking', 'manifest', 'prepare', 'patch']);
40
41
  const ResourceModNames = ['strings', 'colors', 'media'];
41
- exports.HarmonyModNames = Object.freeze([
42
+ const AllModNames = Object.freeze([
42
43
  'dangerous',
44
+ 'prepare',
43
45
  'reactNativeConfig',
44
46
  'appJson',
45
47
  'projectBuildProfile',
@@ -62,8 +64,10 @@ exports.HarmonyModNames = Object.freeze([
62
64
  'cppPackageProvider',
63
65
  'cmakeLists',
64
66
  'autolinking',
67
+ 'patch',
65
68
  'manifest',
66
69
  ]);
70
+ exports.HarmonyModNames = Object.freeze(AllModNames.filter((name) => name !== 'manifest' && name !== 'nativeInputsStamp' && name !== 'prepare' && name !== 'patch'));
67
71
  function recordManagedFile(config, file, owner) {
68
72
  config._internal ??= {};
69
73
  config._internal.harmonyManagedFiles ??= [];
@@ -73,35 +77,41 @@ function recordManagedFile(config, file, owner) {
73
77
  files.sort((left, right) => left.path.localeCompare(right.path, 'en'));
74
78
  config._internal.harmonyManagedFiles = files;
75
79
  }
76
- function assertModResults(modName, value, kind) {
80
+ function assertModResults(name, value, kind) {
77
81
  const valid = kind === 'text'
78
82
  ? typeof value === 'string'
79
83
  : value !== null && typeof value === 'object' && !Array.isArray(value);
80
84
  if (!valid) {
81
- throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_MOD_RESULTS_INVALID', `harmony.${modName} must return ${kind === 'text' ? 'a string' : 'an object'} modResults.`, { operation: `harmony.${modName}.write` });
85
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_MOD_RESULTS_INVALID', `harmony.${name} must return ${kind === 'text' ? 'a string' : 'an object'} modResults.`, { operation: `harmony.${name}.write` });
82
86
  }
83
87
  }
84
- function withFileProvider(config, modName) {
88
+ function withFileProvider(config, name) {
85
89
  return (0, config_plugins_1.withBaseMod)(config, {
86
90
  platform: 'harmony',
87
- mod: modName,
91
+ mod: name,
88
92
  isProvider: true,
89
- isIntrospective: JsonMods.has(modName),
90
- saveToInternal: JsonMods.has(modName),
93
+ isIntrospective: JsonMods.has(name),
94
+ saveToInternal: JsonMods.has(name),
91
95
  async action(value) {
92
- const { nextMod, ...modRequest } = value.modRequest;
93
- const isProjectFile = modName in paths_1.ProjectPaths;
94
- const root = isProjectFile ? modRequest.projectRoot : modRequest.platformProjectRoot;
95
- const file = isProjectFile
96
- ? await (0, paths_1.resolveProjectPath)(root, modName)
97
- : await (0, paths_1.resolveHarmonyPath)(root, paths_1.ManagedPaths[modName]);
98
- const exists = node_fs_1.default.existsSync(file);
99
- const data = JsonMods.has(modName)
100
- ? await (0, files_1.readJson5)(file, {}, modName)
101
- : await (0, files_1.readText)(file);
102
- const request = { ...modRequest, modFile: file, modFileExists: exists };
103
- const result = await nextMod({ ...value, modRequest: request, modResults: data });
104
- assertModResults(modName, result.modResults, JsonMods.has(modName) ? 'json' : 'text');
96
+ const { nextMod: next, ...request } = value.modRequest;
97
+ const project = name in paths_1.ProjectPaths;
98
+ const root = project ? request.projectRoot : request.platformProjectRoot;
99
+ const file = project
100
+ ? await (0, paths_1.resolveProjectPath)(root, name)
101
+ : await (0, paths_1.resolveHarmonyPath)(root, paths_1.ManagedPaths[name]);
102
+ const exists = !request.ignoreExistingNativeFiles && node_fs_1.default.existsSync(file);
103
+ const template = value._internal?.harmonyTemplateDirectory;
104
+ const input = !exists && !project && typeof template === 'string'
105
+ ? await (0, paths_1.resolveHarmonyPath)(template, paths_1.ManagedPaths[name])
106
+ : file;
107
+ const data = request.ignoreExistingNativeFiles && input === file
108
+ ? (JsonMods.has(name) ? {} : '')
109
+ : JsonMods.has(name)
110
+ ? await (0, files_1.readJson5)(input, {}, name)
111
+ : await (0, files_1.readText)(file);
112
+ const context = { ...request, modFile: file, modFileExists: exists };
113
+ const result = await next({ ...value, modRequest: context, modResults: data });
114
+ assertModResults(name, result.modResults, JsonMods.has(name) ? 'json' : 'text');
105
115
  if (!result.modRequest.introspect) {
106
116
  if (typeof result.modResults === 'string') {
107
117
  await (0, files_1.atomicWrite)(file, result.modResults.replace(/\r\n?/g, '\n').replace(/\n?$/, '\n'));
@@ -109,17 +119,19 @@ function withFileProvider(config, modName) {
109
119
  else {
110
120
  await (0, files_1.writeJson5)(file, result.modResults);
111
121
  }
112
- recordManagedFile(result, file, modName);
122
+ recordManagedFile(result, file, name);
113
123
  }
114
124
  return result;
115
125
  },
116
126
  });
117
127
  }
118
- async function readResourceMap(root, entries, modName) {
128
+ async function readResourceMap(root, entries, name, template, ignore = false) {
119
129
  const resources = {};
120
130
  for (const [scope, relative] of Object.entries(entries)) {
121
131
  const file = await (0, paths_1.resolveHarmonyPath)(root, relative);
122
- resources[scope] = await (0, files_1.readJson5)(file, {}, modName);
132
+ const fallback = ignore || !node_fs_1.default.existsSync(file);
133
+ const input = fallback && template ? await (0, paths_1.resolveHarmonyPath)(template, relative) : file;
134
+ resources[scope] = ignore && input === file ? {} : await (0, files_1.readJson5)(input, {}, name);
123
135
  }
124
136
  return resources;
125
137
  }
@@ -162,16 +174,14 @@ async function writeMediaMap(root, media, config, previous = {}) {
162
174
  throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `Harmony media ${scope}/${name} must use a descriptor object.`, { operation: 'harmony.media.write' });
163
175
  }
164
176
  const item = descriptor;
165
- const hasSource = Object.hasOwn(item, 'source');
166
- const hasContent = Object.hasOwn(item, 'content');
167
- if (hasSource === hasContent) {
177
+ if (Object.hasOwn(item, 'source') === Object.hasOwn(item, 'content')) {
168
178
  throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `Harmony media ${scope}/${name} must define exactly one of source or content.`, { operation: 'harmony.media.write' });
169
179
  }
170
180
  if (item.replaceBase !== undefined && typeof item.replaceBase !== 'boolean') {
171
181
  throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `Harmony media ${scope}/${name} replaceBase must be a boolean.`, { operation: 'harmony.media.write' });
172
182
  }
173
183
  let content;
174
- if (hasSource) {
184
+ if (Object.hasOwn(item, 'source')) {
175
185
  if (typeof item.source !== 'string' || item.source.length === 0) {
176
186
  throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `Harmony media ${scope}/${name} source must be a non-empty path.`, { operation: 'harmony.media.write' });
177
187
  }
@@ -191,9 +201,6 @@ async function writeMediaMap(root, media, config, previous = {}) {
191
201
  });
192
202
  }
193
203
  }
194
- // Resolve and read every desired input before removing stale resources. A
195
- // malformed descriptor or missing source therefore cannot partially mutate
196
- // an otherwise valid generated project.
197
204
  for (const [scope, names] of Object.entries(previous)) {
198
205
  if (!(scope in paths_1.ResourcePaths.media))
199
206
  continue;
@@ -210,9 +217,9 @@ async function writeMediaMap(root, media, config, previous = {}) {
210
217
  await node_fs_1.default.promises.mkdir(directory, { recursive: true });
211
218
  if (replaceBase) {
212
219
  const base = node_path_1.default.parse(name).name;
213
- for (const oldName of await node_fs_1.default.promises.readdir(directory)) {
214
- if (node_path_1.default.parse(oldName).name === base && oldName !== name) {
215
- await node_fs_1.default.promises.rm(node_path_1.default.join(directory, oldName), { force: true });
220
+ for (const entry of await node_fs_1.default.promises.readdir(directory)) {
221
+ if (node_path_1.default.parse(entry).name === base && entry !== name) {
222
+ await node_fs_1.default.promises.rm(node_path_1.default.join(directory, entry), { force: true });
216
223
  }
217
224
  }
218
225
  }
@@ -221,47 +228,47 @@ async function writeMediaMap(root, media, config, previous = {}) {
221
228
  recordManagedFile(config, file, 'media');
222
229
  }
223
230
  }
224
- function withResourceProvider(config, modName) {
225
- const isIntrospective = modName !== 'media';
231
+ function withResourceProvider(config, name) {
232
+ const introspective = name !== 'media';
226
233
  return (0, config_plugins_1.withBaseMod)(config, {
227
234
  platform: 'harmony',
228
- mod: modName,
235
+ mod: name,
229
236
  isProvider: true,
230
- isIntrospective,
231
- saveToInternal: isIntrospective,
237
+ isIntrospective: introspective,
238
+ saveToInternal: introspective,
232
239
  async action(value) {
233
- const { nextMod, ...modRequest } = value.modRequest;
234
- const root = modRequest.platformProjectRoot;
235
- const data = modName === 'media'
236
- ? await readMediaMap(root)
237
- : await readResourceMap(root, paths_1.ResourcePaths[modName], modName);
238
- const previous = modName === 'media'
240
+ const { nextMod: next, ...request } = value.modRequest;
241
+ const root = request.platformProjectRoot;
242
+ const data = name === 'media'
243
+ ? (request.ignoreExistingNativeFiles
244
+ ? Object.fromEntries(Object.keys(paths_1.ResourcePaths.media).map(scope => [scope, {}]))
245
+ : await readMediaMap(root))
246
+ : await readResourceMap(root, paths_1.ResourcePaths[name], name, value._internal?.harmonyTemplateDirectory, request.ignoreExistingNativeFiles);
247
+ const previous = name === 'media'
239
248
  ? Object.fromEntries(Object.entries(data).map(([scope, files]) => [scope, Object.keys(files)]))
240
249
  : {};
241
- const result = await nextMod({ ...value, modRequest, modResults: data });
242
- assertModResults(modName, result.modResults, 'resource');
250
+ const result = await next({ ...value, modRequest: request, modResults: data });
251
+ assertModResults(name, result.modResults, 'resource');
243
252
  if (!result.modRequest.introspect) {
244
- if (modName === 'media') {
253
+ if (name === 'media') {
245
254
  await writeMediaMap(root, result.modResults, result, previous);
246
255
  }
247
256
  else {
248
- const paths = paths_1.ResourcePaths[modName];
257
+ const paths = paths_1.ResourcePaths[name];
249
258
  for (const [scope, resource] of Object.entries(result.modResults)) {
250
259
  if (!(scope in paths))
251
260
  continue;
252
- // Harmony's resource compiler requires the JSON root to contain
253
- // exactly one resource-kind member. An empty scope serialized as
254
- // `{}` is invalid, so omit it until a plugin contributes content.
255
261
  if (!resource || typeof resource !== 'object' || Array.isArray(resource)) {
256
- throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `Harmony ${modName} scope ${scope} must be an object.`, { operation: `harmony.${modName}.write` });
262
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `Harmony ${name} scope ${scope} must be an object.`, { operation: `harmony.${name}.write` });
257
263
  }
258
264
  const file = await (0, paths_1.resolveHarmonyPath)(root, paths[scope]);
265
+ // Harmony's resource compiler rejects an empty JSON root.
259
266
  if (Object.keys(resource).length === 0) {
260
267
  await node_fs_1.default.promises.rm(file, { force: true });
261
268
  continue;
262
269
  }
263
270
  await (0, files_1.writeJson5)(file, resource);
264
- recordManagedFile(result, file, modName);
271
+ recordManagedFile(result, file, name);
265
272
  }
266
273
  }
267
274
  }
@@ -269,61 +276,63 @@ function withResourceProvider(config, modName) {
269
276
  },
270
277
  });
271
278
  }
272
- function withVirtualProvider(config, modName) {
279
+ function withVirtualProvider(config, name) {
273
280
  return (0, config_plugins_1.withBaseMod)(config, {
274
281
  platform: 'harmony',
275
- mod: modName,
282
+ mod: name,
276
283
  isProvider: true,
284
+ isIntrospective: name === 'prepare',
277
285
  async action(value) {
278
- const { nextMod, ...modRequest } = value.modRequest;
279
- return nextMod({ ...value, modRequest, modResults: null });
286
+ const { nextMod: next, ...request } = value.modRequest;
287
+ return next({ ...value, modRequest: request, modResults: null });
280
288
  },
281
289
  });
282
290
  }
283
- function withProvider(config, modName) {
291
+ function withProvider(config, name) {
284
292
  const actions = [];
285
293
  config = (0, config_plugins_1.withMod)(config, {
286
294
  platform: 'harmony',
287
- mod: modName,
295
+ mod: name,
288
296
  async action(value) {
289
297
  let result = value;
290
- // withMod is a stack: the last registered plugin runs first. Preserve
291
- // that ordering for Harmony plugins registered after the provider.
298
+ // Late mods must follow Expo's last-registered-first order.
292
299
  for (let index = actions.length - 1; index >= 0; index -= 1) {
293
300
  result = await actions[index](result);
294
301
  }
295
302
  return result;
296
303
  },
297
304
  });
298
- if (JsonMods.has(modName) || TextMods.has(modName)) {
299
- config = withFileProvider(config, modName);
305
+ if (JsonMods.has(name) || TextMods.has(name)) {
306
+ config = withFileProvider(config, name);
300
307
  }
301
- else if (ResourceMods.has(modName)) {
302
- config = withResourceProvider(config, modName);
308
+ else if (ResourceMods.has(name)) {
309
+ config = withResourceProvider(config, name);
303
310
  }
304
- else if (VirtualMods.has(modName)) {
305
- config = withVirtualProvider(config, modName);
311
+ else if (VirtualMods.has(name)) {
312
+ config = withVirtualProvider(config, name);
306
313
  }
307
314
  else {
308
- throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_MOD_NOT_REGISTERED', `Unknown Harmony mod: ${modName}`, { operation: 'register-base-mods' });
315
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_MOD_NOT_REGISTERED', `Unknown Harmony mod: ${name}`, { operation: 'register-base-mods' });
309
316
  }
310
317
  const mods = config.mods;
311
- const provider = mods?.harmony?.[modName];
318
+ const provider = mods?.harmony?.[name];
312
319
  if (provider?.isProvider)
313
320
  provider.expoHarmonyLateActions = actions;
314
321
  return config;
315
322
  }
316
323
  function withHarmonyBaseMods(config) {
317
- for (const modName of exports.HarmonyModNames) {
324
+ for (const name of AllModNames) {
318
325
  const mods = config.mods;
319
- if (!mods?.harmony?.[modName]?.isProvider)
320
- config = withProvider(config, modName);
326
+ if (!mods?.harmony?.[name]?.isProvider)
327
+ config = withProvider(config, name);
321
328
  }
322
329
  const mods = config.mods;
323
330
  const table = mods.harmony;
324
331
  mods.harmony = Object.fromEntries([
325
- ...exports.HarmonyModNames.filter(name => table[name]).map(name => [name, table[name]]),
326
- ...Object.entries(table).filter(([name]) => !exports.HarmonyModNames.includes(name)),
332
+ ...AllModNames.filter(name => table[name] && name !== 'manifest' && name !== 'patch').map(name => [name, table[name]]),
333
+ ...Object.entries(table).filter(([name]) => !AllModNames.includes(name)),
334
+ ['patch', table.patch],
335
+ ['manifest', table.manifest],
327
336
  ]);
328
337
  return config;
329
338
  }
@@ -331,17 +340,32 @@ function withHarmonyMod(config, tuple) {
331
340
  if (!Array.isArray(tuple) || tuple.length !== 2) {
332
341
  throw new TypeError('withHarmonyMod expects [modName, action].');
333
342
  }
334
- const [modName, action] = tuple;
335
- if (!exports.HarmonyModNames.includes(modName) || typeof action !== 'function') {
336
- throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_MOD_NOT_REGISTERED', `Cannot register Harmony mod: ${modName}`, { operation: 'register-mod' });
343
+ const [name, action] = tuple;
344
+ if (!exports.HarmonyModNames.includes(name) || typeof action !== 'function') {
345
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_MOD_NOT_REGISTERED', `Cannot register Harmony mod: ${name}`, { operation: 'register-mod' });
337
346
  }
347
+ return registerMod(config, name, action);
348
+ }
349
+ function registerMod(config, name, action) {
338
350
  const mods = config.mods;
339
- const provider = mods?.harmony?.[modName];
351
+ const provider = mods?.harmony?.[name];
340
352
  if (provider?.isProvider && provider.expoHarmonyLateActions) {
341
- provider.expoHarmonyLateActions.push(action);
353
+ // Build an ordinary Expo interceptor on an isolated mod table, preserving
354
+ // its registration stack, error behavior and execution trace for late mods.
355
+ const carrier = (0, config_plugins_1.withMod)({ ...config, mods: {} }, {
356
+ platform: 'harmony',
357
+ mod: name,
358
+ action: action,
359
+ });
360
+ const interceptor = carrier.mods.harmony[name];
361
+ provider.expoHarmonyLateActions.push(interceptor);
342
362
  return config;
343
363
  }
344
- return (0, config_plugins_1.withMod)(config, { platform: 'harmony', mod: modName, action });
364
+ return (0, config_plugins_1.withMod)(config, {
365
+ platform: 'harmony',
366
+ mod: name,
367
+ action: action,
368
+ });
345
369
  }
346
370
  const withReactNativeConfig = (config, action) => withHarmonyMod(config, ['reactNativeConfig', action]);
347
371
  exports.withReactNativeConfig = withReactNativeConfig;
@@ -353,7 +377,7 @@ const withRootOhPackage = (config, action) => withHarmonyMod(config, ['rootOhPac
353
377
  exports.withRootOhPackage = withRootOhPackage;
354
378
  const withRootHvigor = (config, action) => withHarmonyMod(config, ['rootHvigor', action]);
355
379
  exports.withRootHvigor = withRootHvigor;
356
- const withNativeInputsStamp = (config, action) => withHarmonyMod(config, ['nativeInputsStamp', action]);
380
+ const withNativeInputsStamp = (config, action) => registerMod(config, 'nativeInputsStamp', action);
357
381
  exports.withNativeInputsStamp = withNativeInputsStamp;
358
382
  const withHvigorConfig = (config, action) => withHarmonyMod(config, ['hvigorConfig', action]);
359
383
  exports.withHvigorConfig = withHvigorConfig;
@@ -387,13 +411,17 @@ const withCMakeLists = (config, action) => withHarmonyMod(config, ['cmakeLists',
387
411
  exports.withCMakeLists = withCMakeLists;
388
412
  const withHarmonyAutolinking = (config, action) => withHarmonyMod(config, ['autolinking', action]);
389
413
  exports.withHarmonyAutolinking = withHarmonyAutolinking;
390
- const withCngManifest = (config, action) => withHarmonyMod(config, ['manifest', action]);
414
+ const withCngManifest = (config, action) => registerMod(config, 'manifest', action);
391
415
  exports.withCngManifest = withCngManifest;
416
+ const withPreparation = (config, action) => registerMod(config, 'prepare', action);
417
+ exports.withPreparation = withPreparation;
418
+ const withProjectPatch = (config, action) => registerMod(config, 'patch', action);
419
+ exports.withProjectPatch = withProjectPatch;
392
420
  const withHarmonyDangerousMod = (config, action) => withHarmonyMod(config, ['dangerous', action]);
393
421
  exports.withHarmonyDangerousMod = withHarmonyDangerousMod;
394
422
  const withHarmonyResources = (config, action) => {
395
- for (const modName of ResourceModNames) {
396
- withHarmonyMod(config, [modName, action]);
423
+ for (const name of ResourceModNames) {
424
+ withHarmonyMod(config, [name, action]);
397
425
  }
398
426
  return config;
399
427
  };
@@ -1,4 +1,4 @@
1
- import type { ExpoConfigWithHarmony, HarmonyConfig } from './config';
1
+ import type { ExpoConfigWithHarmony, HarmonyConfig, HarmonyPermission } from './config';
2
2
  type HarmonyExpoConfig = ExpoConfigWithHarmony;
3
3
  interface NormalizedHarmonyConfig {
4
4
  abiFilters: string[];
@@ -22,7 +22,14 @@ interface NormalizedHarmonyConfig {
22
22
  versionCode: number;
23
23
  versionName: string;
24
24
  }
25
- declare const HarmonySdkVersions: Map<number, string>;
25
+ export declare function normalizePermission(permission: HarmonyPermission): {
26
+ name: string;
27
+ reason?: string;
28
+ usedScene?: {
29
+ abilities?: string[];
30
+ when: "always" | "inuse";
31
+ };
32
+ };
26
33
  declare function normalizeHarmonyConfig(config: HarmonyExpoConfig): NormalizedHarmonyConfig;
27
- export { HarmonySdkVersions, normalizeHarmonyConfig };
34
+ export { normalizeHarmonyConfig };
28
35
  export type { HarmonyExpoConfig, NormalizedHarmonyConfig };
@@ -1,12 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HarmonySdkVersions = void 0;
3
+ exports.normalizePermission = normalizePermission;
4
4
  exports.normalizeHarmonyConfig = normalizeHarmonyConfig;
5
5
  const errors_1 = require("./errors");
6
- const BundleNamePattern = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*){2,}$/;
6
+ const resources_1 = require("./resources");
7
+ const BundlePattern = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*){2,}$/;
7
8
  const IdentifierPattern = /^[A-Za-z][A-Za-z0-9_]*$/;
8
9
  const ColorPattern = /^(#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{8})$/;
9
- const ValidOrientations = new Set([
10
+ const Orientations = new Set([
10
11
  'default',
11
12
  'portrait',
12
13
  'landscape',
@@ -14,17 +15,17 @@ const ValidOrientations = new Set([
14
15
  'landscape_inverted',
15
16
  'auto_rotation',
16
17
  ]);
17
- const ValidDeviceTypes = new Set(['phone', 'tablet', '2in1']);
18
- const SupportedMinimumHarmonyApi = 13;
19
- const DefaultHarmonyCompatibleApi = 23;
20
- const DefaultHarmonyTargetApi = 24;
21
- const HarmonySdkVersions = new Map([
18
+ const DeviceTypes = new Set(['phone', 'tablet', '2in1']);
19
+ const MinimumApi = 13;
20
+ const TargetApi = 24;
21
+ const SdkVersions = new Map([
22
22
  [13, '5.0.1(13)'],
23
+ [14, '5.0.2(14)'],
23
24
  [20, '6.0.0(20)'],
25
+ [21, '6.0.1(21)'],
24
26
  [23, '6.1.0(23)'],
25
27
  [24, '6.1.1(24)'],
26
28
  ]);
27
- exports.HarmonySdkVersions = HarmonySdkVersions;
28
29
  class HarmonyConfigError extends errors_1.HarmonyConfigPluginError {
29
30
  constructor(message) {
30
31
  super('ERR_HARMONY_CONFIG_INVALID', message, { operation: 'normalize-config' });
@@ -53,10 +54,7 @@ function normalizeColor(value, field, fallback) {
53
54
  if (typeof color !== 'string' || !ColorPattern.test(color)) {
54
55
  throw new HarmonyConfigError(`${field} must be #RRGGBB or #RRGGBBAA.`);
55
56
  }
56
- const result = color.toUpperCase();
57
- return result.length === 9
58
- ? `#${result.slice(7, 9)}${result.slice(1, 7)}`
59
- : result;
57
+ return (0, resources_1.toArgb)(color);
60
58
  }
61
59
  function parseSdkApi(value, field) {
62
60
  if (value === undefined)
@@ -82,14 +80,14 @@ function resolveSdkVersion(api, field, value) {
82
80
  }
83
81
  else {
84
82
  const version = readString(value, `harmony.${field}`);
85
- const versionApi = parseSdkApi(version, field);
86
- if (versionApi !== api) {
87
- throw new HarmonyConfigError(`harmony.${field} describes API ${versionApi}, but the configured API level is ${api}.`);
83
+ const parsed = parseSdkApi(version, field);
84
+ if (parsed !== api) {
85
+ throw new HarmonyConfigError(`harmony.${field} describes API ${parsed}, but the configured API level is ${api}.`);
88
86
  }
89
87
  return version;
90
88
  }
91
89
  }
92
- const version = HarmonySdkVersions.get(api);
90
+ const version = SdkVersions.get(api);
93
91
  if (!version) {
94
92
  throw new HarmonyConfigError(`Harmony API ${api} has no built-in SDK label. Set harmony.${field} explicitly so new SDK releases do not require a package update.`);
95
93
  }
@@ -157,7 +155,9 @@ function normalizeSkill(skill) {
157
155
  }
158
156
  const result = {};
159
157
  for (const [key, value] of Object.entries(uri)) {
160
- result[key] = readString(value, `harmony.skills[].uris[].${key}`);
158
+ result[key] = typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value))
159
+ ? value
160
+ : readString(value, `harmony.skills[].uris[].${key}`);
161
161
  }
162
162
  return result;
163
163
  });
@@ -176,38 +176,32 @@ function normalizeHarmonyConfig(config) {
176
176
  if (!harmony || typeof harmony !== 'object' || Array.isArray(harmony)) {
177
177
  throw new HarmonyConfigError('Expo config must contain a harmony object.');
178
178
  }
179
- const bundleName = readString(harmony.bundleName, 'harmony.bundleName');
180
- if (!BundleNamePattern.test(bundleName)) {
179
+ const bundle = readString(harmony.bundleName, 'harmony.bundleName');
180
+ if (!BundlePattern.test(bundle)) {
181
181
  throw new HarmonyConfigError('harmony.bundleName must contain at least three valid dot-separated segments.');
182
182
  }
183
- const moduleName = readString(harmony.moduleName, 'harmony.moduleName', 'entry');
184
- const abilityName = readString(harmony.abilityName, 'harmony.abilityName', 'EntryAbility');
185
- if (!IdentifierPattern.test(moduleName)) {
183
+ const module = readString(harmony.moduleName, 'harmony.moduleName', 'entry');
184
+ const ability = readString(harmony.abilityName, 'harmony.abilityName', 'EntryAbility');
185
+ if (!IdentifierPattern.test(module)) {
186
186
  throw new HarmonyConfigError('harmony.moduleName must be a valid Harmony identifier.');
187
187
  }
188
- if (!IdentifierPattern.test(abilityName)) {
188
+ if (!IdentifierPattern.test(ability)) {
189
189
  throw new HarmonyConfigError('harmony.abilityName must be a valid Harmony identifier.');
190
190
  }
191
- const versionName = readString(harmony.versionName, 'harmony.versionName', config.version || '1.0.0');
192
- const versionCode = readPositiveInteger(harmony.versionCode, 'versionCode', 1);
193
- const targetSdkApi = parseSdkApi(harmony.targetSdkVersion, 'targetSdkVersion');
194
- const targetApi = readPositiveInteger(harmony.targetApiVersion, 'targetApiVersion', targetSdkApi ?? DefaultHarmonyTargetApi);
195
- const compatibleSdkApi = parseSdkApi(harmony.compatibleSdkVersion, 'compatibleSdkVersion');
196
- const compatibleApi = readPositiveInteger(compatibleSdkApi ?? undefined, 'compatibleSdkVersion', DefaultHarmonyCompatibleApi);
197
- if (compatibleApi < SupportedMinimumHarmonyApi) {
198
- throw new HarmonyConfigError(`Harmony compatible API must be ${SupportedMinimumHarmonyApi} or newer.`);
191
+ const version = readString(harmony.versionName, 'harmony.versionName', config.version || '1.0.0');
192
+ const code = readPositiveInteger(harmony.versionCode, 'versionCode', 1);
193
+ const target = readPositiveInteger(harmony.targetApiVersion, 'targetApiVersion', parseSdkApi(harmony.targetSdkVersion, 'targetSdkVersion') ?? TargetApi);
194
+ const compatible = readPositiveInteger(parseSdkApi(harmony.compatibleSdkVersion, 'compatibleSdkVersion') ?? undefined, 'compatibleSdkVersion', MinimumApi);
195
+ if (compatible < MinimumApi) {
196
+ throw new HarmonyConfigError(`Harmony compatible API must be ${MinimumApi} or newer.`);
199
197
  }
200
- if (compatibleApi > targetApi) {
198
+ if (compatible > target) {
201
199
  throw new HarmonyConfigError('Harmony compatibleSdkVersion cannot exceed targetApiVersion.');
202
200
  }
203
201
  const orientation = harmony.orientation || config.orientation || 'default';
204
- if (!ValidOrientations.has(orientation)) {
202
+ if (!Orientations.has(orientation)) {
205
203
  throw new HarmonyConfigError(`Unsupported Harmony orientation: ${orientation}`);
206
204
  }
207
- const style = harmony.userInterfaceStyle || config.userInterfaceStyle || 'light';
208
- if (!['light', 'dark', 'automatic'].includes(style)) {
209
- throw new HarmonyConfigError(`Unsupported Harmony UI style: ${style}`);
210
- }
211
205
  const background = normalizeColor(harmony.backgroundColor || config.backgroundColor, 'harmony.backgroundColor', '#FFFFFF');
212
206
  const engine = harmony.jsEngine || 'hermes';
213
207
  if (engine !== 'hermes') {
@@ -250,27 +244,27 @@ function normalizeHarmonyConfig(config) {
250
244
  : readString(harmony.signingConfigFile, 'harmony.signingConfigFile');
251
245
  const devices = harmony.deviceTypes === undefined
252
246
  ? ['phone', 'tablet']
253
- : normalizeStringArray(harmony.deviceTypes, 'harmony.deviceTypes', ValidDeviceTypes);
247
+ : normalizeStringArray(harmony.deviceTypes, 'harmony.deviceTypes', DeviceTypes);
254
248
  return Object.freeze({
255
249
  abiFilters: abis,
256
- abilityName,
250
+ abilityName: ability,
257
251
  backgroundColor: background,
258
- bundleName,
259
- compatibleSdkVersionString: resolveSdkVersion(compatibleApi, 'compatibleSdkVersion', harmony.compatibleSdkVersion),
252
+ bundleName: bundle,
253
+ compatibleSdkVersionString: resolveSdkVersion(compatible, 'compatibleSdkVersion', harmony.compatibleSdkVersion),
260
254
  deviceTypes: devices,
261
255
  icon: harmony.icon || config.icon,
262
256
  label: readString(harmony.label, 'harmony.label', config.name),
263
- moduleName,
257
+ moduleName: module,
264
258
  nativeOrientation: orientation === 'default' ? 'unspecified' : orientation,
265
259
  permissions,
266
260
  productName: readString(harmony.productName, 'harmony.productName', 'default'),
267
261
  querySchemes: queries,
268
262
  signingConfigFile: signing,
269
263
  skills,
270
- targetApiVersion: targetApi,
271
- targetSdkVersionString: resolveSdkVersion(targetApi, 'targetSdkVersion', harmony.targetSdkVersion),
264
+ targetApiVersion: target,
265
+ targetSdkVersionString: resolveSdkVersion(target, 'targetSdkVersion', harmony.targetSdkVersion),
272
266
  vendor: readString(harmony.vendor, 'harmony.vendor', 'expo-harmony'),
273
- versionCode,
274
- versionName,
267
+ versionCode: code,
268
+ versionName: version,
275
269
  });
276
270
  }
@@ -1,6 +1,7 @@
1
- import type { ExpoConfig } from '@expo/config-types';
1
+ import type { ExpoConfigWithHarmony } from './config';
2
2
  export interface HarmonyConfigPluginOwnership {
3
3
  readonly owner: string;
4
+ readonly files?: readonly string[];
4
5
  readonly ability?: Readonly<Partial<{
5
6
  startWindowBackground: string;
6
7
  startWindowIcon: string;
@@ -12,5 +13,5 @@ export interface HarmonyConfigPluginOwnership {
12
13
  }>>;
13
14
  }
14
15
  export declare function normalizeHarmonyConfigPlugins(value: unknown): readonly HarmonyConfigPluginOwnership[];
15
- export declare function registerHarmonyConfigPlugin(config: ExpoConfig, owner: string, claims?: Omit<HarmonyConfigPluginOwnership, 'owner'>): ExpoConfig;
16
- export declare function getHarmonyConfigPlugins(config: ExpoConfig): readonly HarmonyConfigPluginOwnership[];
16
+ export declare function registerHarmonyConfigPlugin<Config extends ExpoConfigWithHarmony>(config: Config, owner: string, claims?: Omit<HarmonyConfigPluginOwnership, 'owner'>): Config;
17
+ export declare function getHarmonyConfigPlugins(config: ExpoConfigWithHarmony): readonly HarmonyConfigPluginOwnership[];
@@ -89,7 +89,17 @@ function normalizeHarmonyConfigPlugins(value) {
89
89
  throw new TypeError(`Invalid Harmony config-plugin ownership: owner ${plugin.owner} is duplicated.`);
90
90
  }
91
91
  owners.add(plugin.owner);
92
+ let files;
93
+ if (plugin.files !== undefined) {
94
+ if (!Array.isArray(plugin.files) || plugin.files.some(file => typeof file !== 'string'
95
+ || !file || file.includes('\\') || file.includes('\0') || /^[A-Za-z]:/.test(file)
96
+ || file.split('/').some(part => !part || part === '.' || part === '..'))) {
97
+ throw new TypeError(`Invalid Harmony config-plugin ownership: ${plugin.owner}.files must contain relative file paths.`);
98
+ }
99
+ files = [...new Set(plugin.files)].sort();
100
+ }
92
101
  return {
102
+ ...(files === undefined ? {} : { files }),
93
103
  ability: normalizeAbility(plugin.ability),
94
104
  owner: plugin.owner,
95
105
  resources: normalizeResources(plugin.resources),
@@ -101,6 +111,13 @@ function registerHarmonyConfigPlugin(config, owner, claims = {}) {
101
111
  const plugin = normalizeHarmonyConfigPlugins([{ ...claims, owner }])[0];
102
112
  config._internal ??= {};
103
113
  const plugins = normalizeHarmonyConfigPlugins(config._internal.harmonyConfigPlugins);
114
+ for (const other of plugins) {
115
+ if (other.owner === owner)
116
+ continue;
117
+ const duplicate = plugin.files?.find(file => other.files?.includes(file));
118
+ if (duplicate)
119
+ throw new TypeError(`Harmony file ${duplicate} is claimed by both ${other.owner} and ${owner}.`);
120
+ }
104
121
  config._internal.harmonyConfigPlugins = normalizeHarmonyConfigPlugins([
105
122
  ...plugins.filter(item => item.owner !== owner),
106
123
  plugin,