@expo-harmony/config-plugins 55.0.10-harmony.1 → 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 (55) 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 +51 -0
  7. package/build/config.js +6 -0
  8. package/build/customFiles.d.ts +15 -0
  9. package/build/customFiles.js +108 -0
  10. package/build/errors.d.ts +11 -0
  11. package/build/errors.js +14 -0
  12. package/build/files.d.ts +6 -0
  13. package/build/files.js +69 -0
  14. package/build/generatedFiles.d.ts +15 -0
  15. package/build/generatedFiles.js +70 -0
  16. package/build/index.d.ts +27 -0
  17. package/build/index.js +52 -6
  18. package/build/internal.d.ts +3 -0
  19. package/build/internal.js +13 -0
  20. package/build/manifest.d.ts +38 -0
  21. package/build/manifest.js +71 -0
  22. package/build/mods.d.ts +57 -0
  23. package/build/mods.js +428 -0
  24. package/build/nativeInputs.d.ts +14 -0
  25. package/build/nativeInputs.js +81 -0
  26. package/build/normalizeConfig.d.ts +35 -0
  27. package/build/normalizeConfig.js +270 -0
  28. package/build/ownership.d.ts +17 -0
  29. package/build/ownership.js +129 -0
  30. package/build/paths.d.ts +64 -0
  31. package/build/paths.js +129 -0
  32. package/build/permissions.d.ts +12 -0
  33. package/build/permissions.js +60 -0
  34. package/build/pluginTypes.d.ts +15 -0
  35. package/build/pluginTypes.js +2 -0
  36. package/build/resources.d.ts +13 -0
  37. package/build/resources.js +55 -0
  38. package/package.json +9 -9
  39. package/src/compiler.ts +21 -0
  40. package/src/composition.ts +45 -0
  41. package/src/config.ts +3 -19
  42. package/src/customFiles.ts +142 -0
  43. package/src/files.ts +3 -4
  44. package/src/generatedFiles.ts +85 -0
  45. package/src/index.ts +19 -11
  46. package/src/internal.ts +3 -0
  47. package/src/manifest.ts +151 -0
  48. package/src/mods.ts +170 -129
  49. package/src/nativeInputs.ts +0 -1
  50. package/src/normalizeConfig.ts +51 -59
  51. package/src/ownership.ts +25 -5
  52. package/src/paths.ts +38 -46
  53. package/src/permissions.ts +68 -0
  54. package/src/pluginTypes.ts +24 -0
  55. package/src/resources.ts +65 -0
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HarmonyPermissions = exports.withHarmonyPermissions = void 0;
4
+ exports.mergePermissions = mergePermissions;
5
+ exports.ensurePermissions = ensurePermissions;
6
+ exports.removePermissions = removePermissions;
7
+ const manifest_1 = require("./manifest");
8
+ const mods_1 = require("./mods");
9
+ const normalizeConfig_1 = require("./normalizeConfig");
10
+ function mergePermissions(current, additions) {
11
+ const result = new Map();
12
+ for (const input of [...current, ...additions]) {
13
+ const permission = (0, normalizeConfig_1.normalizePermission)(input);
14
+ const existing = result.get(permission.name);
15
+ const scene = existing?.usedScene || permission.usedScene;
16
+ const abilities = [...new Set([
17
+ ...(existing?.usedScene?.abilities ?? []),
18
+ ...(permission.usedScene?.abilities ?? []),
19
+ ])];
20
+ result.set(permission.name, {
21
+ ...existing,
22
+ ...permission,
23
+ ...(scene
24
+ ? {
25
+ usedScene: {
26
+ ...(abilities.length ? { abilities } : {}),
27
+ when: existing?.usedScene?.when === 'always' || permission.usedScene?.when === 'always' ? 'always' : 'inuse',
28
+ },
29
+ }
30
+ : {}),
31
+ });
32
+ }
33
+ return [...result.values()];
34
+ }
35
+ function ensurePermissions(manifest, permissions) {
36
+ const module = (0, manifest_1.getModuleOrThrow)(manifest);
37
+ module.requestPermissions = mergePermissions(module.requestPermissions ?? [], permissions);
38
+ return manifest;
39
+ }
40
+ function removePermissions(manifest, names) {
41
+ const module = (0, manifest_1.getModuleOrThrow)(manifest);
42
+ if (module.requestPermissions) {
43
+ module.requestPermissions = module.requestPermissions.filter(permission => !names.includes(permission.name));
44
+ }
45
+ return manifest;
46
+ }
47
+ const withHarmonyPermissions = (config, permissions) => {
48
+ const additions = mergePermissions([], permissions);
49
+ if (config.harmony) {
50
+ config.harmony = { ...config.harmony, permissions: mergePermissions(config.harmony.permissions ?? [], additions) };
51
+ }
52
+ return (0, mods_1.withModuleJson)(config, (mod) => {
53
+ if (mod.modResults.module === undefined)
54
+ mod.modResults.module = {};
55
+ ensurePermissions(mod.modResults, additions);
56
+ return mod;
57
+ });
58
+ };
59
+ exports.withHarmonyPermissions = withHarmonyPermissions;
60
+ exports.HarmonyPermissions = Object.freeze({ ensurePermissions, mergePermissions, removePermissions });
@@ -0,0 +1,15 @@
1
+ import type { ExportedConfigWithProps } from '@expo/config-plugins';
2
+ import type { ExpoConfigWithHarmony, ExpoHarmonyPlatform, HarmonyConfig } from './config';
3
+ export type HarmonyConfigPlugin<Props = void> = <Config extends ExpoConfigWithHarmony>(config: Config, props: Props) => Config;
4
+ export type HarmonyModConfig<T> = Omit<ExportedConfigWithProps<T>, 'platforms' | 'modRawConfig' | 'modRequest'> & {
5
+ platforms?: ExpoHarmonyPlatform[];
6
+ harmony?: HarmonyConfig;
7
+ modRawConfig: ExpoConfigWithHarmony;
8
+ modRequest: Omit<ExportedConfigWithProps<T>['modRequest'], 'platform' | 'nextMod'> & {
9
+ platform: 'harmony';
10
+ nextMod?: HarmonyModAction<T>;
11
+ modFile?: string;
12
+ modFileExists?: boolean;
13
+ };
14
+ };
15
+ export type HarmonyModAction<T = unknown> = (config: HarmonyModConfig<T>) => HarmonyModConfig<T> | Promise<HarmonyModConfig<T>>;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,13 @@
1
+ export interface HarmonyResourceItem extends Record<string, unknown> {
2
+ name: string;
3
+ value: string;
4
+ }
5
+ export interface HarmonyResourceFile extends Record<string, unknown> {
6
+ string?: HarmonyResourceItem[];
7
+ color?: HarmonyResourceItem[];
8
+ }
9
+ export declare function setString(file: HarmonyResourceFile, item: HarmonyResourceItem): HarmonyResourceFile;
10
+ export declare function removeString(file: HarmonyResourceFile, name: string): HarmonyResourceFile;
11
+ export declare function setColor(file: HarmonyResourceFile, item: HarmonyResourceItem): HarmonyResourceFile;
12
+ export declare function removeColor(file: HarmonyResourceFile, name: string): HarmonyResourceFile;
13
+ export declare function toArgb(value: string | number): string;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setString = setString;
4
+ exports.removeString = removeString;
5
+ exports.setColor = setColor;
6
+ exports.removeColor = removeColor;
7
+ exports.toArgb = toArgb;
8
+ const errors_1 = require("./errors");
9
+ function setItem(file, kind, item) {
10
+ if (!item || typeof item.name !== 'string' || !/^[A-Za-z0-9_.-]+$/.test(item.name)
11
+ || item.name.includes('..') || typeof item.value !== 'string') {
12
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_RESOURCE_INVALID', `Invalid Harmony ${kind} resource.`, { operation: 'edit-resource' });
13
+ }
14
+ const items = readItems(file, kind);
15
+ const existing = items.find(value => value?.name === item.name);
16
+ file[kind] = [...items.filter(value => value?.name !== item.name), { ...existing, ...item }];
17
+ return file;
18
+ }
19
+ function readItems(file, kind) {
20
+ if (file[kind] !== undefined && !Array.isArray(file[kind])) {
21
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_RESOURCE_INVALID', `Harmony ${kind} resources must be an array.`, { operation: 'edit-resource' });
22
+ }
23
+ return file[kind] ?? [];
24
+ }
25
+ function setString(file, item) {
26
+ return setItem(file, 'string', item);
27
+ }
28
+ function removeString(file, name) {
29
+ const items = readItems(file, 'string');
30
+ if (file.string)
31
+ file.string = items.filter(item => item?.name !== name);
32
+ return file;
33
+ }
34
+ function setColor(file, item) {
35
+ return setItem(file, 'color', item);
36
+ }
37
+ function removeColor(file, name) {
38
+ const items = readItems(file, 'color');
39
+ if (file.color)
40
+ file.color = items.filter(item => item?.name !== name);
41
+ return file;
42
+ }
43
+ function toArgb(value) {
44
+ let hex;
45
+ if (typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 0xffffffff) {
46
+ hex = value.toString(16).padStart(8, '0');
47
+ }
48
+ else if (typeof value === 'string' && /^#[0-9a-f]{6}(?:[0-9a-f]{2})?$/i.test(value)) {
49
+ hex = value.slice(1);
50
+ }
51
+ else {
52
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', 'Color must be #RRGGBB, #RRGGBBAA or an unsigned RGBA integer.', { operation: 'convert-color' });
53
+ }
54
+ return `#${hex.length === 8 ? hex.slice(6, 8) + hex.slice(0, 6) : hex}`.toUpperCase();
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo-harmony/config-plugins",
3
- "version": "55.0.10-harmony.1",
3
+ "version": "55.0.10-harmony.3",
4
4
  "keywords": [
5
5
  "react-native",
6
6
  "expo",
@@ -29,12 +29,17 @@
29
29
  "require": "./build/index.js",
30
30
  "default": "./build/index.js"
31
31
  },
32
- "./native-inputs": {
32
+ "./package.json": "./package.json",
33
+ "./internal": {
34
+ "types": "./build/internal.d.ts",
35
+ "require": "./build/internal.js",
36
+ "default": "./build/internal.js"
37
+ },
38
+ "./internal/native-inputs": {
33
39
  "types": "./build/nativeInputs.d.ts",
34
40
  "require": "./build/nativeInputs.js",
35
41
  "default": "./build/nativeInputs.js"
36
- },
37
- "./package.json": "./package.json"
42
+ }
38
43
  },
39
44
  "dependencies": {
40
45
  "@expo/config-plugins": "55.0.10",
@@ -44,11 +49,6 @@
44
49
  "peerDependencies": {
45
50
  "@expo/config": "55.0.17"
46
51
  },
47
- "peerDependenciesMeta": {
48
- "@expo/config": {
49
- "optional": true
50
- }
51
- },
52
52
  "devDependencies": {
53
53
  "@types/node": "18.19.0",
54
54
  "typescript": "^6.0.3"
@@ -0,0 +1,21 @@
1
+ import { compileModsAsync, type ExportedConfig, type ModPlatform } from '@expo/config-plugins';
2
+
3
+ import type { ExpoConfigWithHarmony } from './config';
4
+ import { withHarmonyBaseMods } from './mods';
5
+
6
+ export interface HarmonyCompileOptions {
7
+ projectRoot: string;
8
+ introspect?: boolean;
9
+ ignoreExistingNativeFiles?: boolean;
10
+ }
11
+
12
+ export async function compileHarmonyModsAsync<Config extends ExpoConfigWithHarmony>(
13
+ config: Config,
14
+ options: HarmonyCompileOptions
15
+ ): Promise<Config> {
16
+ return await compileModsAsync(withHarmonyBaseMods(config) as ExportedConfig, {
17
+ ...options,
18
+ platforms: ['harmony' as ModPlatform],
19
+ assertMissingModProviders: true,
20
+ }) as Config;
21
+ }
@@ -0,0 +1,45 @@
1
+ import {
2
+ createRunOncePlugin as expoCreateRunOncePlugin,
3
+ withPlugins as expoWithPlugins,
4
+ withRunOnce as expoWithRunOnce,
5
+ withStaticPlugin as expoWithStaticPlugin,
6
+ type ConfigPlugin,
7
+ } from '@expo/config-plugins';
8
+
9
+ import type { HarmonyConfigPlugin } from './pluginTypes';
10
+ import type { ExpoConfigWithHarmony } from './config';
11
+
12
+ // Expo plugins accept the narrower platform union. Method variance allows them
13
+ // alongside Harmony plugins without losing contextual types for inline callbacks.
14
+ type CompatiblePlugin<Props = void> = {
15
+ apply(config: ExpoConfigWithHarmony, props: Props): ExpoConfigWithHarmony;
16
+ }['apply'];
17
+
18
+ export type HarmonyPluginReference = string | CompatiblePlugin<never>;
19
+ export type HarmonyStaticPlugin = HarmonyPluginReference | [HarmonyPluginReference, unknown];
20
+
21
+ export const withPlugins = expoWithPlugins as unknown as HarmonyConfigPlugin<HarmonyStaticPlugin[]>;
22
+ export const withRunOnce = expoWithRunOnce as unknown as HarmonyConfigPlugin<{
23
+ plugin: CompatiblePlugin;
24
+ name: string;
25
+ version?: string;
26
+ }>;
27
+ export const withStaticPlugin = expoWithStaticPlugin as unknown as HarmonyConfigPlugin<{
28
+ plugin: HarmonyStaticPlugin;
29
+ projectRoot?: string;
30
+ fallback?: CompatiblePlugin<{ _resolverError: Error } & Record<string, unknown>>;
31
+ }>;
32
+
33
+ export function createRunOncePlugin<Props>(
34
+ plugin: HarmonyConfigPlugin<Props>, name: string, version?: string
35
+ ): HarmonyConfigPlugin<Props>;
36
+ export function createRunOncePlugin<Props>(
37
+ plugin: ConfigPlugin<Props>, name: string, version?: string
38
+ ): HarmonyConfigPlugin<Props>;
39
+ export function createRunOncePlugin(
40
+ plugin: ConfigPlugin<never> | HarmonyConfigPlugin<never>,
41
+ name: string,
42
+ version?: string
43
+ ): HarmonyConfigPlugin<never> {
44
+ return expoCreateRunOncePlugin(plugin as ConfigPlugin<never>, name, version) as HarmonyConfigPlugin<never>;
45
+ }
package/src/config.ts CHANGED
@@ -18,25 +18,14 @@ export interface HarmonyPermission {
18
18
  usedScene?: { abilities?: string[]; when?: 'inuse' | 'always' };
19
19
  }
20
20
 
21
+ export type HarmonySkillUri = Record<string, string | number | boolean>;
22
+
21
23
  export interface HarmonySkill {
22
24
  entities?: string[];
23
25
  actions?: string[];
24
- uris?: Array<Record<string, string>>;
25
- }
26
-
27
- export interface HarmonyFontDefinition {
28
- path: string;
29
- weight?: number;
30
- style?: 'normal' | 'italic';
26
+ uris?: HarmonySkillUri[];
31
27
  }
32
28
 
33
- export type HarmonyFont
34
- = | string
35
- | {
36
- fontFamily: string;
37
- fontDefinitions: HarmonyFontDefinition[];
38
- };
39
-
40
29
  export interface HarmonyConfig {
41
30
  bundleName: string;
42
31
  moduleName?: string;
@@ -46,14 +35,11 @@ export interface HarmonyConfig {
46
35
  versionCode?: number;
47
36
  versionName?: string;
48
37
  targetApiVersion?: number;
49
- /** Harmony SDK label written to build-profile.json5, e.g. 6.0.0(20). Its API must match targetApiVersion. */
50
38
  targetSdkVersion?: number | string;
51
- /** API number, or a full Harmony SDK label ending in the API number. */
52
39
  compatibleSdkVersion?: number | string;
53
40
  deviceTypes?: HarmonyDeviceType[];
54
41
  permissions?: HarmonyPermission[];
55
42
  skills?: HarmonySkill[];
56
- /** URL schemes the app may inspect with Linking.canOpenURL. http, https, and expo.scheme are added automatically. */
57
43
  querySchemes?: string[];
58
44
  icon?: string;
59
45
  label?: string;
@@ -63,8 +49,6 @@ export interface HarmonyConfig {
63
49
  jsEngine?: 'hermes';
64
50
  abiFilters?: string[];
65
51
  signingConfigFile?: string;
66
- /** Fonts bundled when the `@expo-harmony/expo-font` config plugin is registered. */
67
- fonts?: HarmonyFont[];
68
52
  }
69
53
 
70
54
  export type ExpoConfigWithHarmony = Omit<ExpoConfig, 'platforms'> & {
@@ -0,0 +1,142 @@
1
+ import fs from 'node:fs/promises';
2
+
3
+ import { withBaseMod, type ExportedConfig, type ModPlatform } from '@expo/config-plugins';
4
+ import JSON5 from 'json5';
5
+
6
+ import { HarmonyConfigPluginError } from './errors';
7
+ import { atomicWrite, stableJson } from './files';
8
+ import { recordManagedFile, registerMod } from './mods';
9
+ import { getHarmonyConfigPlugins, normalizeHarmonyConfigPlugins, registerHarmonyConfigPlugin } from './ownership';
10
+ import { ManagedPaths, ResourcePaths, resolveHarmonyPath } from './paths';
11
+ import type { HarmonyConfigPlugin, HarmonyModAction, HarmonyModConfig } from './pluginTypes';
12
+
13
+ export interface HarmonyFileModOptions<T> {
14
+ path: string;
15
+ owner: string;
16
+ parse: (source: string | null) => T;
17
+ serialize: (value: T) => string;
18
+ isIntrospective?: boolean;
19
+ }
20
+
21
+ type Definition = Omit<HarmonyFileModOptions<unknown>, 'parse' | 'serialize'> & { parse: unknown; serialize: unknown };
22
+ interface CustomProvider { expoHarmonyLateActions?: unknown }
23
+ const providers = new WeakMap<object, Definition>();
24
+
25
+ export function createHarmonyFileMod<T>(options: HarmonyFileModOptions<T>): HarmonyConfigPlugin<HarmonyModAction<T>> {
26
+ const definition = Object.freeze({ ...options });
27
+ const { path: relative, owner, parse, serialize, isIntrospective = false } = definition;
28
+
29
+ normalizeHarmonyConfigPlugins([{ owner, files: [relative] }]);
30
+
31
+ const reserved = Object.values(ManagedPaths).includes(relative)
32
+ || Object.values(ResourcePaths).some(scopes => Object.values(scopes).some(file => relative === file))
33
+ || Object.values(ResourcePaths.media).some(directory => relative.startsWith(`${directory}/`));
34
+ if (reserved || relative.split('/').some(part => part === '.git') || relative.startsWith('.expo/')) {
35
+ throw new HarmonyConfigPluginError('ERR_HARMONY_PATH_INVALID', `Use the built-in mod for reserved path ${relative}.`);
36
+ }
37
+ if (typeof parse !== 'function' || typeof serialize !== 'function') {
38
+ throw new TypeError('A file mod requires parse and serialize functions.');
39
+ }
40
+
41
+ const name = `file:${relative}`;
42
+
43
+ return (config, action) => {
44
+ const mods = (config as ExportedConfig).mods as Record<string, Record<string, CustomProvider>> | undefined;
45
+ const provider = mods?.harmony?.[name];
46
+ if (provider) {
47
+ const stored = providers.get(provider);
48
+ if (!stored || stored.owner !== owner || stored.parse !== parse
49
+ || stored.serialize !== serialize || !!stored.isIntrospective !== isIntrospective) {
50
+ throw new HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `Conflicting providers for ${relative}. Reuse the same file mod definition.`);
51
+ }
52
+ }
53
+
54
+ const claims = getHarmonyConfigPlugins(config).find(plugin => plugin.owner === owner);
55
+ registerHarmonyConfigPlugin(config, owner, {
56
+ ...claims, files: [...new Set([...(claims?.files ?? []), relative])],
57
+ });
58
+
59
+ if (!provider) {
60
+ const actions: HarmonyModAction<T>[] = [];
61
+ withBaseMod<T>(config as ExportedConfig, {
62
+ platform: 'harmony' as ModPlatform,
63
+ mod: name,
64
+ isProvider: true,
65
+ isIntrospective,
66
+ saveToInternal: isIntrospective,
67
+ async action(value) {
68
+ const { nextMod: _next, ...request } = value.modRequest;
69
+ const file = await resolveHarmonyPath(request.platformProjectRoot, relative);
70
+ let source: string | null = null;
71
+
72
+ if (!request.ignoreExistingNativeFiles) {
73
+ try {
74
+ source = await fs.readFile(file, 'utf8');
75
+ } catch (cause) {
76
+ if ((cause as NodeJS.ErrnoException).code !== 'ENOENT') {
77
+ throw new HarmonyConfigPluginError(
78
+ 'ERR_HARMONY_TEXT_INVALID',
79
+ `Cannot read ${file}: ${(cause as Error).message}`,
80
+ { cause, file, operation: `harmony.${name}.read` }
81
+ );
82
+ }
83
+ }
84
+ }
85
+
86
+ let result = {
87
+ ...value,
88
+ modRequest: { ...request, modFile: file, modFileExists: source !== null },
89
+ modResults: parse(source),
90
+ } as unknown as HarmonyModConfig<T>;
91
+ for (let index = actions.length - 1; index >= 0; index--) {
92
+ result = await actions[index](result) as HarmonyModConfig<T>;
93
+ }
94
+
95
+ const output = serialize(result.modResults);
96
+ if (typeof output !== 'string') throw new TypeError(`Serializer for ${relative} must return a string.`);
97
+
98
+ if (!request.introspect) {
99
+ await atomicWrite(file, output);
100
+ recordManagedFile(result, file, owner);
101
+ }
102
+
103
+ return result as unknown as typeof value;
104
+ },
105
+ });
106
+
107
+ const table = ((config as ExportedConfig).mods as Record<string, Record<string, CustomProvider>>).harmony;
108
+ table[name].expoHarmonyLateActions = actions;
109
+ providers.set(table[name], definition);
110
+
111
+ // CNG must fingerprint custom outputs after they have been written.
112
+ for (const stage of ['patch', 'manifest']) {
113
+ if (table[stage]) {
114
+ const provider = table[stage];
115
+ delete table[stage];
116
+ table[stage] = provider;
117
+ }
118
+ }
119
+ }
120
+
121
+ return registerMod(config, name, action);
122
+ };
123
+ }
124
+
125
+ function parseJson(source: string | null): Record<string, unknown> {
126
+ const value = source === null ? {} : JSON5.parse(source);
127
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
128
+ throw new TypeError('A Harmony JSON file must contain an object.');
129
+ }
130
+
131
+ return value;
132
+ }
133
+
134
+ export interface HarmonyJsonFileOptions {
135
+ path: string;
136
+ owner: string;
137
+ action: HarmonyModAction<Record<string, unknown>>;
138
+ }
139
+
140
+ export const withHarmonyJsonFile: HarmonyConfigPlugin<HarmonyJsonFileOptions> = (config, { path, owner, action }) => (
141
+ createHarmonyFileMod({ path, owner, parse: parseJson, serialize: stableJson, isIntrospective: true })(config, action)
142
+ );
package/src/files.ts CHANGED
@@ -21,7 +21,7 @@ export function stableJson(value: unknown): string {
21
21
  return `${JSON.stringify(sortValue(value), null, 2)}\n`;
22
22
  }
23
23
 
24
- export async function readJson5<T>(file: string, fallback: T, modName: string): Promise<T> {
24
+ export async function readJson5<T>(file: string, fallback: T, name: string): Promise<T> {
25
25
  try {
26
26
  const source = await fs.promises.readFile(file, 'utf8');
27
27
 
@@ -31,8 +31,8 @@ export async function readJson5<T>(file: string, fallback: T, modName: string):
31
31
 
32
32
  throw new HarmonyConfigPluginError(
33
33
  'ERR_HARMONY_JSON5_INVALID',
34
- `Cannot parse ${file} for harmony.${modName}: ${(cause as Error).message}`,
35
- { cause, file, operation: `harmony.${modName}.read` }
34
+ `Cannot parse ${file} for harmony.${name}: ${(cause as Error).message}`,
35
+ { cause, file, operation: `harmony.${name}.read` }
36
36
  );
37
37
  }
38
38
  }
@@ -74,6 +74,5 @@ export async function writeJson5(file: string, value: unknown): Promise<void> {
74
74
 
75
75
  export async function sha256File(file: string): Promise<string> {
76
76
  const data = Uint8Array.from(await fs.promises.readFile(file));
77
-
78
77
  return crypto.createHash('sha256').update(data).digest('hex');
79
78
  }
@@ -0,0 +1,85 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { HarmonyConfigPluginError } from './errors';
5
+ import { atomicWrite } from './files';
6
+ import { recordManagedFile, withHarmonyDangerousMod } from './mods';
7
+ import { getHarmonyConfigPlugins, registerHarmonyConfigPlugin } from './ownership';
8
+ import { resolveHarmonyPath } from './paths';
9
+ import type { HarmonyConfigPlugin } from './pluginTypes';
10
+
11
+ export type HarmonyFileDescriptor
12
+ = | { source: string; content?: never }
13
+ | { source?: never; content: string | Uint8Array };
14
+ export type HarmonyFileMap = Record<string, HarmonyFileDescriptor>;
15
+ export interface HarmonyGeneratedFilesOptions {
16
+ owner: string;
17
+ files: HarmonyFileMap;
18
+ }
19
+
20
+ export const withHarmonyGeneratedFiles: HarmonyConfigPlugin<HarmonyGeneratedFilesOptions> = (config, options) => {
21
+ const { owner, files } = options;
22
+ if (!files || typeof files !== 'object' || Array.isArray(files)) {
23
+ throw new HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', 'Generated files must be a path-to-descriptor map.');
24
+ }
25
+
26
+ const claims = getHarmonyConfigPlugins(config).find(plugin => plugin.owner === owner);
27
+ const paths = [...new Set([...(claims?.files ?? []), ...Object.keys(files)])];
28
+ config = registerHarmonyConfigPlugin(config, owner, { ...claims, files: paths });
29
+
30
+ return withHarmonyDangerousMod(config, async (mod) => {
31
+ if (mod.modRequest.introspect) return mod;
32
+
33
+ const writes: Array<{ file: string; content: string | Uint8Array }> = [];
34
+ for (const [relative, input] of Object.entries(files)) {
35
+ if (!input || typeof input !== 'object'
36
+ || Object.hasOwn(input, 'source') === Object.hasOwn(input, 'content')) {
37
+ throw new HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `${relative} must define exactly one of source or content.`);
38
+ }
39
+
40
+ const file = await resolveHarmonyPath(mod.modRequest.platformProjectRoot, relative);
41
+ let content: string | Uint8Array;
42
+ if (Object.hasOwn(input, 'source')) {
43
+ if (typeof input.source !== 'string' || !input.source) {
44
+ throw new HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `${relative} source must be a non-empty path.`);
45
+ }
46
+
47
+ const source = path.resolve(mod.modRequest.projectRoot, input.source);
48
+ content = Uint8Array.from(await fs.promises.readFile(source));
49
+ } else {
50
+ if (typeof input.content !== 'string' && !(input.content instanceof Uint8Array)) {
51
+ throw new HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `${relative} content must be a string or Uint8Array.`);
52
+ }
53
+
54
+ content = input.content;
55
+ }
56
+
57
+ writes.push({ file, content });
58
+ }
59
+
60
+ for (const { file, content } of writes) {
61
+ await atomicWrite(file, content);
62
+ recordManagedFile(mod, file, owner);
63
+ }
64
+
65
+ return mod;
66
+ });
67
+ };
68
+
69
+ export const withRawfile: HarmonyConfigPlugin<HarmonyGeneratedFilesOptions> = (config, { owner, files }) => {
70
+ if (!files || typeof files !== 'object' || Array.isArray(files)) {
71
+ throw new HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', 'Rawfiles must be a path-to-descriptor map.');
72
+ }
73
+
74
+ const prefix = 'entry/src/main/resources/rawfile/';
75
+ const output: HarmonyFileMap = {};
76
+ for (const [relative, input] of Object.entries(files)) {
77
+ if (!relative || relative.includes('\\') || relative.split('/').some(part => !part || part === '.' || part === '..')) {
78
+ throw new HarmonyConfigPluginError('ERR_HARMONY_PATH_ESCAPE', `Invalid rawfile path: ${relative}`);
79
+ }
80
+
81
+ output[prefix + relative] = input;
82
+ }
83
+
84
+ return withHarmonyGeneratedFiles(config, { owner, files: output });
85
+ };
package/src/index.ts CHANGED
@@ -1,20 +1,26 @@
1
1
  export { defineExpoHarmonyConfig } from './config';
2
+ export { createRunOncePlugin, withPlugins, withRunOnce, withStaticPlugin } from './composition';
3
+ export type { HarmonyPluginReference, HarmonyStaticPlugin } from './composition';
4
+ export { compileHarmonyModsAsync } from './compiler';
5
+ export type { HarmonyCompileOptions } from './compiler';
6
+ export { createHarmonyFileMod, withHarmonyJsonFile } from './customFiles';
7
+ export type { HarmonyFileModOptions, HarmonyJsonFileOptions } from './customFiles';
2
8
  export type {
3
9
  ExpoConfigWithHarmony,
4
10
  ExpoHarmonyPlatform,
5
11
  ExpoKnownPlatform,
6
12
  HarmonyConfig,
7
13
  HarmonyDeviceType,
8
- HarmonyFont,
9
- HarmonyFontDefinition,
10
14
  HarmonyOrientation,
11
15
  HarmonyPermission,
12
16
  HarmonyPlatform,
13
17
  HarmonySkill,
18
+ HarmonySkillUri,
14
19
  } from './config';
15
20
  export { HarmonyConfigPluginError } from './errors';
21
+ export type { HarmonyConfigPluginErrorOptions } from './errors';
16
22
  export { atomicWrite, stableJson as stableHarmonyJson } from './files';
17
- export { HarmonySdkVersions, normalizeHarmonyConfig } from './normalizeConfig';
23
+ export { normalizeHarmonyConfig } from './normalizeConfig';
18
24
  export type { HarmonyExpoConfig, NormalizedHarmonyConfig } from './normalizeConfig';
19
25
  export {
20
26
  HarmonyModNames as HARMONY_MOD_NAMES,
@@ -22,7 +28,6 @@ export {
22
28
  withAppJson,
23
29
  withArkTSPackageProvider,
24
30
  withCMakeLists,
25
- withCngManifest,
26
31
  withColors,
27
32
  withCppPackageProvider,
28
33
  withEntryAbility,
@@ -38,7 +43,6 @@ export {
38
43
  withIndexPage,
39
44
  withMedia,
40
45
  withModuleJson,
41
- withNativeInputsStamp,
42
46
  withProfiles,
43
47
  withProjectBuildProfile,
44
48
  withReactNativeConfig,
@@ -51,15 +55,10 @@ export type {
51
55
  HarmonyJson,
52
56
  HarmonyMediaDescriptor,
53
57
  HarmonyMediaMap,
54
- HarmonyModAction,
55
58
  HarmonyModName,
56
59
  HarmonyResourceMap,
57
60
  } from './mods';
58
- export {
59
- getHarmonyConfigPlugins,
60
- normalizeHarmonyConfigPlugins,
61
- registerHarmonyConfigPlugin,
62
- } from './ownership';
61
+ export { registerHarmonyConfigPlugin } from './ownership';
63
62
  export type { HarmonyConfigPluginOwnership } from './ownership';
64
63
  export { HarmonyPaths } from './paths';
65
64
  export type {
@@ -69,3 +68,12 @@ export type {
69
68
  HarmonyProjectPaths,
70
69
  HarmonyResourcePaths,
71
70
  } from './paths';
71
+
72
+ export type { HarmonyConfigPlugin, HarmonyModAction, HarmonyModConfig } from './pluginTypes';
73
+ export * as HarmonyManifest from './manifest';
74
+ export * as HarmonyResources from './resources';
75
+ export type { HarmonyAbility, HarmonyExtensionAbility, HarmonyMetadata, HarmonyModule, HarmonyModuleJson } from './manifest';
76
+ export type { HarmonyResourceFile, HarmonyResourceItem } from './resources';
77
+ export { withHarmonyGeneratedFiles, withRawfile } from './generatedFiles';
78
+ export type { HarmonyFileDescriptor, HarmonyFileMap, HarmonyGeneratedFilesOptions } from './generatedFiles';
79
+ export { HarmonyPermissions, withHarmonyPermissions } from './permissions';
@@ -0,0 +1,3 @@
1
+ export { getHarmonyConfigPlugins, normalizeHarmonyConfigPlugins } from './ownership';
2
+ export { withCngManifest, withNativeInputsStamp, withPreparation, withProjectPatch } from './mods';
3
+ export { NativeInputsStampPath } from './paths';