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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +203 -11
  2. package/build/compiler.d.ts +7 -0
  3. package/build/compiler.js +12 -0
  4. package/build/composition.d.ts +24 -0
  5. package/build/composition.js +11 -0
  6. package/build/config.d.ts +2 -15
  7. package/build/customFiles.d.ts +15 -0
  8. package/build/customFiles.js +108 -0
  9. package/build/files.d.ts +1 -1
  10. package/build/files.js +2 -2
  11. package/build/generatedFiles.d.ts +15 -0
  12. package/build/generatedFiles.js +70 -0
  13. package/build/index.d.ts +20 -5
  14. package/build/index.js +53 -6
  15. package/build/internal.d.ts +3 -0
  16. package/build/internal.js +13 -0
  17. package/build/manifest.d.ts +38 -0
  18. package/build/manifest.js +71 -0
  19. package/build/mods.d.ts +37 -31
  20. package/build/mods.js +118 -86
  21. package/build/normalizeConfig.d.ts +10 -3
  22. package/build/normalizeConfig.js +45 -47
  23. package/build/ownership.d.ts +4 -3
  24. package/build/ownership.js +17 -0
  25. package/build/paths.d.ts +13 -10
  26. package/build/paths.js +32 -31
  27. package/build/permissions.d.ts +12 -0
  28. package/build/permissions.js +60 -0
  29. package/build/pluginTypes.d.ts +15 -0
  30. package/build/pluginTypes.js +2 -0
  31. package/build/resources.d.ts +13 -0
  32. package/build/resources.js +55 -0
  33. package/package.json +9 -9
  34. package/src/compiler.ts +21 -0
  35. package/src/composition.ts +45 -0
  36. package/src/config.ts +3 -19
  37. package/src/customFiles.ts +142 -0
  38. package/src/files.ts +3 -4
  39. package/src/generatedFiles.ts +85 -0
  40. package/src/index.ts +20 -11
  41. package/src/internal.ts +3 -0
  42. package/src/manifest.ts +151 -0
  43. package/src/mods.ts +180 -128
  44. package/src/nativeInputs.ts +0 -1
  45. package/src/normalizeConfig.ts +56 -59
  46. package/src/ownership.ts +25 -5
  47. package/src/paths.ts +41 -45
  48. package/src/permissions.ts +68 -0
  49. package/src/pluginTypes.ts +24 -0
  50. package/src/resources.ts +65 -0
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [**GitHub 仓库**](https://github.com/renbaoshuo/expo-harmony/tree/master/packages/config-plugins)
4
4
 
5
- 为 Expo Config Plugins 提供 HarmonyOS 平台扩展,包括 `expo.harmony` 配置类型、Harmony Base Mods、原生工程文件与资源 Mod、路径解析及插件资源归属管理。
5
+ 这个包定义 `expo.harmony` 的字段和默认值,并提供读写 Harmony 原生工程的 Mod、资源与权限操作,以及插件生成文件的归属管理。
6
6
 
7
7
  ## 安装
8
8
 
@@ -10,9 +10,9 @@
10
10
  npm install @expo-harmony/config-plugins
11
11
  ```
12
12
 
13
- ## 配置类型
13
+ ## 声明 HarmonyOS 配置
14
14
 
15
- 在 `app.config.js` 中可使用 `defineExpoHarmonyConfig` 声明 HarmonyOS 配置:
15
+ 用 `defineExpoHarmonyConfig` 在 `app.config.js` 里写配置:
16
16
 
17
17
  ```js
18
18
  const { defineExpoHarmonyConfig } = require('@expo-harmony/config-plugins');
@@ -29,9 +29,40 @@ module.exports = defineExpoHarmonyConfig({
29
29
  });
30
30
  ```
31
31
 
32
- ## 编写 HarmonyOS 配置插件
32
+ `defineExpoHarmonyConfig` 里的 `harmony` 字段可以写这些配置:
33
33
 
34
- 使用对应的 Mod 修改生成后的 HarmonyOS 原生配置。例如,向 `module.json5` 添加信息:
34
+ | 字段 | 默认值 / 说明 |
35
+ | --- | --- |
36
+ | `bundleName` | 必填,至少三段点分隔的合法标识符 |
37
+ | `moduleName` | `entry` |
38
+ | `abilityName` | `EntryAbility` |
39
+ | `productName` | `default` |
40
+ | `vendor` | `expo-harmony` |
41
+ | `versionCode` | `1` |
42
+ | `versionName` | 配置里的 `version`,没有则 `1.0.0` |
43
+ | `targetApiVersion` | `24` |
44
+ | `targetSdkVersion` | 数字或 SDK 标签,默认跟随 `targetApiVersion` |
45
+ | `compatibleSdkVersion` | `13`,不能低于 13,也不能大于 `targetApiVersion` |
46
+ | `deviceTypes` | `['phone', 'tablet']` |
47
+ | `permissions` | 权限列表,默认包含 `ohos.permission.INTERNET` |
48
+ | `skills` | 技能列表,`scheme` 会追加对应项 |
49
+ | `querySchemes` | 默认包含 `http`、`https`、`tel`、`sms` 和 `scheme` |
50
+ | `icon` | 配置里的 `icon` |
51
+ | `label` | 配置里的 `name` |
52
+ | `backgroundColor` | 配置里的 `backgroundColor`,没有则 `#FFFFFF` |
53
+ | `orientation` | 配置里的 `orientation`,没有则 `default` |
54
+ | `userInterfaceStyle` | 供 `expo-system-ui` 和 `expo-splash-screen` 插件读取的 HarmonyOS 外观覆盖值,由模块插件校验和应用 |
55
+ | `jsEngine` | `hermes` |
56
+ | `abiFilters` | `['arm64-v8a', 'x86_64']` |
57
+ | `signingConfigFile` | 签名配置文件路径,不设置则忽略 |
58
+
59
+ `targetSdkVersion` 和 `compatibleSdkVersion` 可以写数字或带 API 级别的 SDK 标签。内置标签覆盖 API 13、14、20、21、23、24,其他版本写成 `6.1.1(24)` 这样的形式。`backgroundColor` 写 `#RRGGBB` 或 `#RRGGBBAA`,写入原生工程时转成 ARGB。`jsEngine` 目前只能是 `hermes`。
60
+
61
+ `normalizeHarmonyConfig(config)` 返回补齐默认值并校验后的配置。
62
+
63
+ ## 修改原生工程文件
64
+
65
+ Mod 读写原生工程里的文件。以 `module.json5` 为例:
35
66
 
36
67
  ```js
37
68
  const { withModuleJson } = require('@expo-harmony/config-plugins');
@@ -39,9 +70,7 @@ const { withModuleJson } = require('@expo-harmony/config-plugins');
39
70
  module.exports = function withExampleMetadata(config) {
40
71
  return withModuleJson(config, (mod) => {
41
72
  const moduleConfig = mod.modResults.module || {};
42
- const metadata = Array.isArray(moduleConfig.metadata)
43
- ? moduleConfig.metadata
44
- : [];
73
+ const metadata = Array.isArray(moduleConfig.metadata) ? moduleConfig.metadata : [];
45
74
 
46
75
  mod.modResults.module = {
47
76
  ...moduleConfig,
@@ -52,13 +81,176 @@ module.exports = function withExampleMetadata(config) {
52
81
  };
53
82
  ```
54
83
 
55
- 包内提供 JSON、文本、资源、自动链接等 Harmony Mod。常用入口包括 `withModuleJson`、`withAppJson`、`withStrings`、`withColors`、`withMedia`、`withCMakeLists`、`withHarmonyDangerousMod` 等,可以在 export 中查看完整列表。
84
+ 回调在 `mod.modResults` 上读写并返回 `mod`。JSON 文件的 `modResults` 是对象,文本文件是字符串,返回结果写回文件。同一个 Mod 注册多次时,后注册的回调先执行。需要读最终原始配置时用 `mod.modRawConfig`。
85
+
86
+ 内置 Mod 与目标文件:
87
+
88
+ | 接口 | 文件 |
89
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------- |
90
+ | `withAppJson` | `AppScope/app.json5` |
91
+ | `withProjectBuildProfile` | `build-profile.json5` |
92
+ | `withRootOhPackage` | `oh-package.json5` |
93
+ | `withHvigorConfig` | `hvigor/hvigor-config.json5` |
94
+ | `withEntryBuildProfile` | `entry/build-profile.json5` |
95
+ | `withEntryOhPackage` | `entry/oh-package.json5` |
96
+ | `withModuleJson` | `entry/src/main/module.json5` |
97
+ | `withProfiles` | `entry/src/main/resources/base/profile/main_pages.json` |
98
+ | `withReactNativeConfig` | `react-native.config.js` |
99
+ | `withRootHvigor` | `hvigorfile.ts` |
100
+ | `withEntryHvigor` | `entry/hvigorfile.ts` |
101
+ | `withEntryAbility` | `entry/src/main/ets/entryability/EntryAbility.ets` |
102
+ | `withIndexPage` | `entry/src/main/ets/pages/Index.ets` |
103
+ | `withWorker` | `entry/src/main/ets/workers/RNOHWorker.ets` |
104
+ | `withArkTSPackageProvider` | `entry/src/main/ets/PackageProvider.ets` |
105
+ | `withCppPackageProvider` | `entry/src/main/cpp/PackageProvider.cpp` |
106
+ | `withCMakeLists` | `entry/src/main/cpp/CMakeLists.txt` |
107
+ | `withStrings` | `AppScope/resources/base/element/string.json`、`entry/src/main/resources/base/element/string.json` |
108
+ | `withColors` | `entry/src/main/resources/base/element/color.json`、`entry/src/main/resources/dark/element/color.json` |
109
+ | `withMedia` | `AppScope/resources/base/media`、`entry/src/main/resources/base/media`、`entry/src/main/resources/dark/media` |
110
+
111
+ `withHarmonyMod(config, [name, action])` 按名称注册 Mod,`HARMONY_MOD_NAMES` 列出可用名称。`withHarmonyResources` 把同一个 action 注册到 strings、colors、media。`withHarmonyDangerousMod` 用来改写没有固定目标文件的内容。`withHarmonyAutolinking` 执行原生模块自动链接。`withHarmonyBaseMods` 注册整套 Harmony Base Mod。
112
+
113
+ `compileHarmonyModsAsync(config, { projectRoot, introspect?, ignoreExistingNativeFiles? })` 直接执行 Mods。`introspect: true` 时只跑静态 Mod,不写文件,结果保存在 `_internal.modResults.harmony`,自定义文件以 `file:<相对路径>` 为键。从 `expo/config` 读配置时要设 `isModdedConfig: true`,否则 Mods 会丢失。
114
+
115
+ `HarmonyPaths` 提供工程内路径常量(`HARMONY_PATHS`、`PROJECT_PATHS`、`PROJECT_PATH_CANDIDATES`、`RESOURCE_PATHS`)和路径解析、边界检查函数。
116
+
117
+ ## Manifest
118
+
119
+ `HarmonyManifest` 原地修改和查询 `module.json5`:
120
+
121
+ - `getModuleOrThrow(manifest)` 读取 `module`,缺失或格式不对时抛错。
122
+ - `getMainAbilityOrThrow(manifest, name?)` 按显式名称、`mainElement`、第一个 Ability 的顺序选取,目标缺失或重名时抛错。
123
+ - `setMetadata(target, item)`、`removeMetadata(target, name)` 按名称更新 module 或 Ability 的 metadata,保留其他条目和字段。
124
+ - `ensureExtensionAbility(manifest, ability)` 添加 ExtensionAbility,重复调用不会追加,同名声明存在不兼容字段时抛错。
125
+
126
+ `withModuleJson` 的结果是 `HarmonyModuleJson`。`HarmonyModule`、`HarmonyAbility`、`HarmonyExtensionAbility`、`HarmonyMetadata` 保留未知字段,可以使用新 SDK 的字段。`HarmonySkillUri` 的值可以是字符串、数字或布尔值。
127
+
128
+ ```js
129
+ const { HarmonyManifest, withModuleJson } = require('@expo-harmony/config-plugins');
130
+
131
+ function withExample(config) {
132
+ return withModuleJson(config, (mod) => {
133
+ const module = HarmonyManifest.getModuleOrThrow(mod.modResults);
134
+ HarmonyManifest.setMetadata(module, { name: 'example.enabled', value: 'true' });
135
+
136
+ const ability = HarmonyManifest.getMainAbilityOrThrow(mod.modResults);
137
+ ability.backgroundModes = [...new Set([...(ability.backgroundModes || []), 'audioPlayback'])];
138
+ return mod;
139
+ });
140
+ }
141
+ ```
142
+
143
+ ## 权限
144
+
145
+ `withHarmonyPermissions(config, permissions)` 把权限合并进 `expo.harmony.permissions` 和生成的 `module.json5`。权限名用完整的 `ohos.permission.*`。同名权限合并 `usedScene.abilities`,任一声明要求 `always` 就保留 `always`,后传入的 `reason` 覆盖旧值。`reason` 写 `$string:...` 这样的原生引用,文案放在字符串资源里。
146
+
147
+ `HarmonyPermissions.mergePermissions(current, additions)` 返回合并后的新数组。`ensurePermissions(manifest, additions)` 和 `removePermissions(manifest, names)` 修改并返回 manifest。删除只改应用 manifest 里列出的声明,不会动 HAR 自己声明的权限。
148
+
149
+ ## 字符串、颜色和媒体
150
+
151
+ `HarmonyResources.setString(file, { name, value })`、`removeString(file, name)`、`setColor`、`removeColor` 原地修改单个资源 JSON 对象并返回,按名称去重,保留其他内容。空字符串是合法值,删除要显式调用 remove。
152
+
153
+ ```js
154
+ const { HarmonyResources, registerHarmonyConfigPlugin, withStrings } = require('@expo-harmony/config-plugins');
155
+
156
+ function withExampleReason(config, reason) {
157
+ config = registerHarmonyConfigPlugin(config, 'example-plugin', {
158
+ resources: { strings: { entry: ['example_permission_reason'] } },
159
+ });
160
+ return withStrings(config, (mod) => {
161
+ const entry = (mod.modResults.entry ??= {});
162
+ HarmonyResources.setString(entry, { name: 'example_permission_reason', value: reason });
163
+ return mod;
164
+ });
165
+ }
166
+ ```
167
+
168
+ `setColor` 接收原生颜色值,不改通道顺序。`HarmonyResources.toArgb` 把 `#RRGGBB`、Expo 的 `#RRGGBBAA` 或无符号 RGBA 整数转成 Harmony 颜色。命名颜色先用调用方的颜色解析器转成 RGBA 整数。
169
+
170
+ `withStrings` 改的是 `app` 和 `entry`,`withColors` 改的是 `entry` 和 `entryDark`。`withMedia` 在 `app`、`entry` 或 `entryDark` 里按文件名设置 `{ source }` 或 `{ content }`,`replaceBase: true` 会替换同基本名的其他扩展名。
171
+
172
+ ## Rawfile 和生成文件
173
+
174
+ ```js
175
+ const { withRawfile, withHarmonyGeneratedFiles } = require('@expo-harmony/config-plugins');
176
+
177
+ function withExampleFiles(config) {
178
+ config = withRawfile(config, {
179
+ owner: 'example-plugin',
180
+ files: {
181
+ 'audio/example.wav': { source: './assets/example.wav' },
182
+ 'example.json': { content: JSON.stringify({ enabled: true }) },
183
+ },
184
+ });
185
+ return withHarmonyGeneratedFiles(config, {
186
+ owner: 'example-plugin',
187
+ files: {
188
+ 'entry/src/main/ets/example/Example.ets': { content: 'export const enabled = true;\n' },
189
+ },
190
+ });
191
+ }
192
+ ```
193
+
194
+ `withRawfile` 的路径相对 `entry/src/main/resources/rawfile`,`withHarmonyGeneratedFiles` 的路径相对 Harmony 工程根目录。路径用 `/`,不接受绝对路径、`.`、`..` 和指向工程外的符号链接。`source` 相对 Expo 项目根目录,也可以用绝对路径;`content` 是字符串或 `Uint8Array`,和 `source` 二选一。
195
+
196
+ 两个接口会覆盖自己声明的目标文件,执行时读取源文件,先校验整批输入再原子写入,并登记文件归属。introspection 阶段不写文件。多个插件不能声明同一个目标路径,同一个 owner 的多次调用合并成这次配置的完整文件集合。
197
+
198
+ 配合 `@expo-harmony/prebuild-config` 时,下次生成会清掉同一 owner 不再声明的旧文件,卸载插件会清理它管理的文件。字体这类需要动态计算的场景可以用 `withHarmonyDangerousMod`、`atomicWrite` 和 `recordManagedFile`。
199
+
200
+ ## 自定义文件
201
+
202
+ `withHarmonyJsonFile` 管理一个 JSON 或 JSON5 文件:
203
+
204
+ ```js
205
+ const { withHarmonyJsonFile } = require('@expo-harmony/config-plugins');
206
+
207
+ module.exports = (config) =>
208
+ withHarmonyJsonFile(config, {
209
+ path: 'entry/src/main/resources/zh_CN/element/string.json',
210
+ owner: 'my-library/locales',
211
+ action: (mod) => {
212
+ mod.modResults.string = [{ name: 'my_message', value: '你好' }];
213
+ return mod;
214
+ },
215
+ });
216
+ ```
217
+
218
+ 一个文件由一个 owner 管理。其他插件用相同的 `path` 和 `owner` 继续修改时,文件只读写一次;不同 owner 声明同一路径会报错。多个插件改资源数组时按名称合并,避免覆盖别人的条目。
219
+
220
+ `createHarmonyFileMod({ path, owner, parse, serialize, isIntrospective? })` 用来定义其他文本格式,返回和 `withModuleJson` 一样接收 `(config, action)` 的函数。`parse` 接收文件内容或表示文件不存在的 `null`,`serialize` 返回字符串,两者是纯函数。序列化对象时可以用 `stableHarmonyJson`,它按固定键序输出。定义一次并导出,供其他插件复用。只有静态、可 JSON 序列化、不产生文件副作用的 Mod 才能设 `isIntrospective: true`,JSON 快捷接口已经开启。
221
+
222
+ provider 会校验路径、登记归属并原子写入,移除 provider 后 prebuild 会清理它生成的文件。它管理整个文件,不会单独撤销某个 action 对字段的修改。内置 Mod 已经管理的路径不能再声明。
223
+
224
+ ## 声明文件与资源归属
225
+
226
+ `registerHarmonyConfigPlugin(config, owner, claims)` 声明插件占用的文件和资源名。`files` 是相对路径列表,`resources` 按 `strings`、`colors`、`media` 分组,每组再按作用域列出名称,`ability` 现在能写 `startWindowBackground` 和 `startWindowIcon`。不同 owner 声明同一个文件会报错。
227
+
228
+ ```js
229
+ const { registerHarmonyConfigPlugin } = require('@expo-harmony/config-plugins');
230
+
231
+ module.exports = (config) =>
232
+ registerHarmonyConfigPlugin(config, 'my-plugin', {
233
+ files: ['entry/src/main/resources/base/profile/shortcuts.json'],
234
+ resources: {
235
+ colors: { entry: ['brand_primary'] },
236
+ strings: { app: ['app_display_name'] },
237
+ },
238
+ });
239
+ ```
240
+
241
+ `recordManagedFile(config, file, owner)` 单独登记一个受管理文件,适合动态生成的文件。
242
+
243
+ ## 插件组合
244
+
245
+ `withPlugins`、`withRunOnce`、`withStaticPlugin`、`createRunOncePlugin` 可以写 `platforms: ['harmony']`,并保留调用方的配置类型。其他 Expo 通用工具从项目的 `expo/config-plugins` 取。
246
+
247
+ ## 类型
56
248
 
57
- `withHarmonyBaseMods` 负责注册所有 Harmony Base Mods,通常由 `@expo-harmony/prebuild-config` 调用,应用和第三方配置插件无需重复注册。`registerHarmonyConfigPlugin` 可声明插件拥有的资源或 Ability 字段,避免多个插件在生成阶段产生归属冲突。
249
+ `HarmonyConfigPlugin<Props>` 保留传入配置的具体类型,可以写 `platforms: ['harmony']`。`HarmonyModAction<T>` 和 `HarmonyModConfig<T>` 里的 Harmony 配置、`modRawConfig.harmony`、`modRequest.platform: 'harmony'` 和结果都有类型。文件 Mod 还能读到可选的 `modRequest.modFile` 和 `modFileExists`。
58
250
 
59
251
  ## Author
60
252
 
61
253
  **expo-harmony** © [Baoshuo](https://github.com/renbaoshuo), Released under the MIT License.<br>
62
254
  Authored and maintained by Baoshuo with help from [contributors](https://github.com/renbaoshuo/expo-harmony/contributors).
63
255
 
64
- > [Personal Website](https://baoshuo.ren) · [Blog](https://blog.baoshuo.ren) · GitHub [@renbaoshuo](https://github.com/renbaoshuo) · Twitter [@baoshuo](https://twitter.com/baoshuo)
256
+ > [Personal Website](https://baoshuo.ren) · [Blog](https://blog.baoshuo.ren) · GitHub [@renbaoshuo](https://github.com/renbaoshuo) · Twitter [@renbaoshuo](https://twitter.com/renbaoshuo)
@@ -0,0 +1,7 @@
1
+ import type { ExpoConfigWithHarmony } from './config';
2
+ export interface HarmonyCompileOptions {
3
+ projectRoot: string;
4
+ introspect?: boolean;
5
+ ignoreExistingNativeFiles?: boolean;
6
+ }
7
+ export declare function compileHarmonyModsAsync<Config extends ExpoConfigWithHarmony>(config: Config, options: HarmonyCompileOptions): Promise<Config>;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.compileHarmonyModsAsync = compileHarmonyModsAsync;
4
+ const config_plugins_1 = require("@expo/config-plugins");
5
+ const mods_1 = require("./mods");
6
+ async function compileHarmonyModsAsync(config, options) {
7
+ return await (0, config_plugins_1.compileModsAsync)((0, mods_1.withHarmonyBaseMods)(config), {
8
+ ...options,
9
+ platforms: ['harmony'],
10
+ assertMissingModProviders: true,
11
+ });
12
+ }
@@ -0,0 +1,24 @@
1
+ import { type ConfigPlugin } from '@expo/config-plugins';
2
+ import type { HarmonyConfigPlugin } from './pluginTypes';
3
+ import type { ExpoConfigWithHarmony } from './config';
4
+ type CompatiblePlugin<Props = void> = {
5
+ apply(config: ExpoConfigWithHarmony, props: Props): ExpoConfigWithHarmony;
6
+ }['apply'];
7
+ export type HarmonyPluginReference = string | CompatiblePlugin<never>;
8
+ export type HarmonyStaticPlugin = HarmonyPluginReference | [HarmonyPluginReference, unknown];
9
+ export declare const withPlugins: HarmonyConfigPlugin<HarmonyStaticPlugin[]>;
10
+ export declare const withRunOnce: HarmonyConfigPlugin<{
11
+ plugin: CompatiblePlugin;
12
+ name: string;
13
+ version?: string;
14
+ }>;
15
+ export declare const withStaticPlugin: HarmonyConfigPlugin<{
16
+ plugin: HarmonyStaticPlugin;
17
+ projectRoot?: string;
18
+ fallback?: CompatiblePlugin<{
19
+ _resolverError: Error;
20
+ } & Record<string, unknown>>;
21
+ }>;
22
+ export declare function createRunOncePlugin<Props>(plugin: HarmonyConfigPlugin<Props>, name: string, version?: string): HarmonyConfigPlugin<Props>;
23
+ export declare function createRunOncePlugin<Props>(plugin: ConfigPlugin<Props>, name: string, version?: string): HarmonyConfigPlugin<Props>;
24
+ export {};
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withStaticPlugin = exports.withRunOnce = exports.withPlugins = void 0;
4
+ exports.createRunOncePlugin = createRunOncePlugin;
5
+ const config_plugins_1 = require("@expo/config-plugins");
6
+ exports.withPlugins = config_plugins_1.withPlugins;
7
+ exports.withRunOnce = config_plugins_1.withRunOnce;
8
+ exports.withStaticPlugin = config_plugins_1.withStaticPlugin;
9
+ function createRunOncePlugin(plugin, name, version) {
10
+ return (0, config_plugins_1.createRunOncePlugin)(plugin, name, version);
11
+ }
package/build/config.d.ts CHANGED
@@ -12,20 +12,12 @@ export interface HarmonyPermission {
12
12
  when?: 'inuse' | 'always';
13
13
  };
14
14
  }
15
+ export type HarmonySkillUri = Record<string, string | number | boolean>;
15
16
  export interface HarmonySkill {
16
17
  entities?: string[];
17
18
  actions?: string[];
18
- uris?: Array<Record<string, string>>;
19
+ uris?: HarmonySkillUri[];
19
20
  }
20
- export interface HarmonyFontDefinition {
21
- path: string;
22
- weight?: number;
23
- style?: 'normal' | 'italic';
24
- }
25
- export type HarmonyFont = string | {
26
- fontFamily: string;
27
- fontDefinitions: HarmonyFontDefinition[];
28
- };
29
21
  export interface HarmonyConfig {
30
22
  bundleName: string;
31
23
  moduleName?: string;
@@ -35,14 +27,11 @@ export interface HarmonyConfig {
35
27
  versionCode?: number;
36
28
  versionName?: string;
37
29
  targetApiVersion?: number;
38
- /** Harmony SDK label written to build-profile.json5, e.g. 6.0.0(20). Its API must match targetApiVersion. */
39
30
  targetSdkVersion?: number | string;
40
- /** API number, or a full Harmony SDK label ending in the API number. */
41
31
  compatibleSdkVersion?: number | string;
42
32
  deviceTypes?: HarmonyDeviceType[];
43
33
  permissions?: HarmonyPermission[];
44
34
  skills?: HarmonySkill[];
45
- /** URL schemes the app may inspect with Linking.canOpenURL. http, https, and expo.scheme are added automatically. */
46
35
  querySchemes?: string[];
47
36
  icon?: string;
48
37
  label?: string;
@@ -52,8 +41,6 @@ export interface HarmonyConfig {
52
41
  jsEngine?: 'hermes';
53
42
  abiFilters?: string[];
54
43
  signingConfigFile?: string;
55
- /** Fonts bundled when the `@expo-harmony/expo-font` config plugin is registered. */
56
- fonts?: HarmonyFont[];
57
44
  }
58
45
  export type ExpoConfigWithHarmony = Omit<ExpoConfig, 'platforms'> & {
59
46
  platforms?: ExpoHarmonyPlatform[];
@@ -0,0 +1,15 @@
1
+ import type { HarmonyConfigPlugin, HarmonyModAction } from './pluginTypes';
2
+ export interface HarmonyFileModOptions<T> {
3
+ path: string;
4
+ owner: string;
5
+ parse: (source: string | null) => T;
6
+ serialize: (value: T) => string;
7
+ isIntrospective?: boolean;
8
+ }
9
+ export declare function createHarmonyFileMod<T>(options: HarmonyFileModOptions<T>): HarmonyConfigPlugin<HarmonyModAction<T>>;
10
+ export interface HarmonyJsonFileOptions {
11
+ path: string;
12
+ owner: string;
13
+ action: HarmonyModAction<Record<string, unknown>>;
14
+ }
15
+ export declare const withHarmonyJsonFile: HarmonyConfigPlugin<HarmonyJsonFileOptions>;
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.withHarmonyJsonFile = void 0;
7
+ exports.createHarmonyFileMod = createHarmonyFileMod;
8
+ const promises_1 = __importDefault(require("node:fs/promises"));
9
+ const config_plugins_1 = require("@expo/config-plugins");
10
+ const json5_1 = __importDefault(require("json5"));
11
+ const errors_1 = require("./errors");
12
+ const files_1 = require("./files");
13
+ const mods_1 = require("./mods");
14
+ const ownership_1 = require("./ownership");
15
+ const paths_1 = require("./paths");
16
+ const providers = new WeakMap();
17
+ function createHarmonyFileMod(options) {
18
+ const definition = Object.freeze({ ...options });
19
+ const { path: relative, owner, parse, serialize, isIntrospective = false } = definition;
20
+ (0, ownership_1.normalizeHarmonyConfigPlugins)([{ owner, files: [relative] }]);
21
+ const reserved = Object.values(paths_1.ManagedPaths).includes(relative)
22
+ || Object.values(paths_1.ResourcePaths).some(scopes => Object.values(scopes).some(file => relative === file))
23
+ || Object.values(paths_1.ResourcePaths.media).some(directory => relative.startsWith(`${directory}/`));
24
+ if (reserved || relative.split('/').some(part => part === '.git') || relative.startsWith('.expo/')) {
25
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_PATH_INVALID', `Use the built-in mod for reserved path ${relative}.`);
26
+ }
27
+ if (typeof parse !== 'function' || typeof serialize !== 'function') {
28
+ throw new TypeError('A file mod requires parse and serialize functions.');
29
+ }
30
+ const name = `file:${relative}`;
31
+ return (config, action) => {
32
+ const mods = config.mods;
33
+ const provider = mods?.harmony?.[name];
34
+ if (provider) {
35
+ const stored = providers.get(provider);
36
+ if (!stored || stored.owner !== owner || stored.parse !== parse
37
+ || stored.serialize !== serialize || !!stored.isIntrospective !== isIntrospective) {
38
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `Conflicting providers for ${relative}. Reuse the same file mod definition.`);
39
+ }
40
+ }
41
+ const claims = (0, ownership_1.getHarmonyConfigPlugins)(config).find(plugin => plugin.owner === owner);
42
+ (0, ownership_1.registerHarmonyConfigPlugin)(config, owner, {
43
+ ...claims, files: [...new Set([...(claims?.files ?? []), relative])],
44
+ });
45
+ if (!provider) {
46
+ const actions = [];
47
+ (0, config_plugins_1.withBaseMod)(config, {
48
+ platform: 'harmony',
49
+ mod: name,
50
+ isProvider: true,
51
+ isIntrospective,
52
+ saveToInternal: isIntrospective,
53
+ async action(value) {
54
+ const { nextMod: _next, ...request } = value.modRequest;
55
+ const file = await (0, paths_1.resolveHarmonyPath)(request.platformProjectRoot, relative);
56
+ let source = null;
57
+ if (!request.ignoreExistingNativeFiles) {
58
+ try {
59
+ source = await promises_1.default.readFile(file, 'utf8');
60
+ }
61
+ catch (cause) {
62
+ if (cause.code !== 'ENOENT') {
63
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_TEXT_INVALID', `Cannot read ${file}: ${cause.message}`, { cause, file, operation: `harmony.${name}.read` });
64
+ }
65
+ }
66
+ }
67
+ let result = {
68
+ ...value,
69
+ modRequest: { ...request, modFile: file, modFileExists: source !== null },
70
+ modResults: parse(source),
71
+ };
72
+ for (let index = actions.length - 1; index >= 0; index--) {
73
+ result = await actions[index](result);
74
+ }
75
+ const output = serialize(result.modResults);
76
+ if (typeof output !== 'string')
77
+ throw new TypeError(`Serializer for ${relative} must return a string.`);
78
+ if (!request.introspect) {
79
+ await (0, files_1.atomicWrite)(file, output);
80
+ (0, mods_1.recordManagedFile)(result, file, owner);
81
+ }
82
+ return result;
83
+ },
84
+ });
85
+ const table = config.mods.harmony;
86
+ table[name].expoHarmonyLateActions = actions;
87
+ providers.set(table[name], definition);
88
+ // CNG must fingerprint custom outputs after they have been written.
89
+ for (const stage of ['patch', 'manifest']) {
90
+ if (table[stage]) {
91
+ const provider = table[stage];
92
+ delete table[stage];
93
+ table[stage] = provider;
94
+ }
95
+ }
96
+ }
97
+ return (0, mods_1.registerMod)(config, name, action);
98
+ };
99
+ }
100
+ function parseJson(source) {
101
+ const value = source === null ? {} : json5_1.default.parse(source);
102
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
103
+ throw new TypeError('A Harmony JSON file must contain an object.');
104
+ }
105
+ return value;
106
+ }
107
+ const withHarmonyJsonFile = (config, { path, owner, action }) => (createHarmonyFileMod({ path, owner, parse: parseJson, serialize: files_1.stableJson, isIntrospective: true })(config, action));
108
+ exports.withHarmonyJsonFile = withHarmonyJsonFile;
package/build/files.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export declare function stableJson(value: unknown): string;
2
- export declare function readJson5<T>(file: string, fallback: T, modName: string): Promise<T>;
2
+ export declare function readJson5<T>(file: string, fallback: T, name: string): Promise<T>;
3
3
  export declare function readText(file: string, fallback?: string): Promise<string>;
4
4
  export declare function atomicWrite(file: string, content: string | Uint8Array): Promise<void>;
5
5
  export declare function writeJson5(file: string, value: unknown): Promise<void>;
package/build/files.js CHANGED
@@ -27,7 +27,7 @@ function sortValue(value) {
27
27
  function stableJson(value) {
28
28
  return `${JSON.stringify(sortValue(value), null, 2)}\n`;
29
29
  }
30
- async function readJson5(file, fallback, modName) {
30
+ async function readJson5(file, fallback, name) {
31
31
  try {
32
32
  const source = await node_fs_1.default.promises.readFile(file, 'utf8');
33
33
  return json5_1.default.parse(source);
@@ -35,7 +35,7 @@ async function readJson5(file, fallback, modName) {
35
35
  catch (cause) {
36
36
  if (cause.code === 'ENOENT')
37
37
  return structuredClone(fallback);
38
- throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_JSON5_INVALID', `Cannot parse ${file} for harmony.${modName}: ${cause.message}`, { cause, file, operation: `harmony.${modName}.read` });
38
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_JSON5_INVALID', `Cannot parse ${file} for harmony.${name}: ${cause.message}`, { cause, file, operation: `harmony.${name}.read` });
39
39
  }
40
40
  }
41
41
  async function readText(file, fallback = '') {
@@ -0,0 +1,15 @@
1
+ import type { HarmonyConfigPlugin } from './pluginTypes';
2
+ export type HarmonyFileDescriptor = {
3
+ source: string;
4
+ content?: never;
5
+ } | {
6
+ source?: never;
7
+ content: string | Uint8Array;
8
+ };
9
+ export type HarmonyFileMap = Record<string, HarmonyFileDescriptor>;
10
+ export interface HarmonyGeneratedFilesOptions {
11
+ owner: string;
12
+ files: HarmonyFileMap;
13
+ }
14
+ export declare const withHarmonyGeneratedFiles: HarmonyConfigPlugin<HarmonyGeneratedFilesOptions>;
15
+ export declare const withRawfile: HarmonyConfigPlugin<HarmonyGeneratedFilesOptions>;
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.withRawfile = exports.withHarmonyGeneratedFiles = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const errors_1 = require("./errors");
10
+ const files_1 = require("./files");
11
+ const mods_1 = require("./mods");
12
+ const ownership_1 = require("./ownership");
13
+ const paths_1 = require("./paths");
14
+ const withHarmonyGeneratedFiles = (config, options) => {
15
+ const { owner, files } = options;
16
+ if (!files || typeof files !== 'object' || Array.isArray(files)) {
17
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', 'Generated files must be a path-to-descriptor map.');
18
+ }
19
+ const claims = (0, ownership_1.getHarmonyConfigPlugins)(config).find(plugin => plugin.owner === owner);
20
+ const paths = [...new Set([...(claims?.files ?? []), ...Object.keys(files)])];
21
+ config = (0, ownership_1.registerHarmonyConfigPlugin)(config, owner, { ...claims, files: paths });
22
+ return (0, mods_1.withHarmonyDangerousMod)(config, async (mod) => {
23
+ if (mod.modRequest.introspect)
24
+ return mod;
25
+ const writes = [];
26
+ for (const [relative, input] of Object.entries(files)) {
27
+ if (!input || typeof input !== 'object'
28
+ || Object.hasOwn(input, 'source') === Object.hasOwn(input, 'content')) {
29
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `${relative} must define exactly one of source or content.`);
30
+ }
31
+ const file = await (0, paths_1.resolveHarmonyPath)(mod.modRequest.platformProjectRoot, relative);
32
+ let content;
33
+ if (Object.hasOwn(input, 'source')) {
34
+ if (typeof input.source !== 'string' || !input.source) {
35
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `${relative} source must be a non-empty path.`);
36
+ }
37
+ const source = node_path_1.default.resolve(mod.modRequest.projectRoot, input.source);
38
+ content = Uint8Array.from(await node_fs_1.default.promises.readFile(source));
39
+ }
40
+ else {
41
+ if (typeof input.content !== 'string' && !(input.content instanceof Uint8Array)) {
42
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `${relative} content must be a string or Uint8Array.`);
43
+ }
44
+ content = input.content;
45
+ }
46
+ writes.push({ file, content });
47
+ }
48
+ for (const { file, content } of writes) {
49
+ await (0, files_1.atomicWrite)(file, content);
50
+ (0, mods_1.recordManagedFile)(mod, file, owner);
51
+ }
52
+ return mod;
53
+ });
54
+ };
55
+ exports.withHarmonyGeneratedFiles = withHarmonyGeneratedFiles;
56
+ const withRawfile = (config, { owner, files }) => {
57
+ if (!files || typeof files !== 'object' || Array.isArray(files)) {
58
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', 'Rawfiles must be a path-to-descriptor map.');
59
+ }
60
+ const prefix = 'entry/src/main/resources/rawfile/';
61
+ const output = {};
62
+ for (const [relative, input] of Object.entries(files)) {
63
+ if (!relative || relative.includes('\\') || relative.split('/').some(part => !part || part === '.' || part === '..')) {
64
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_PATH_ESCAPE', `Invalid rawfile path: ${relative}`);
65
+ }
66
+ output[prefix + relative] = input;
67
+ }
68
+ return (0, exports.withHarmonyGeneratedFiles)(config, { owner, files: output });
69
+ };
70
+ exports.withRawfile = withRawfile;
package/build/index.d.ts CHANGED
@@ -1,12 +1,27 @@
1
1
  export { defineExpoHarmonyConfig } from './config';
2
- export type { ExpoConfigWithHarmony, ExpoHarmonyPlatform, ExpoKnownPlatform, HarmonyConfig, HarmonyDeviceType, HarmonyFont, HarmonyFontDefinition, HarmonyOrientation, HarmonyPermission, HarmonyPlatform, HarmonySkill, } 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';
8
+ export type { ExpoConfigWithHarmony, ExpoHarmonyPlatform, ExpoKnownPlatform, HarmonyConfig, HarmonyDeviceType, HarmonyOrientation, HarmonyPermission, HarmonyPlatform, HarmonySkill, HarmonySkillUri, } from './config';
3
9
  export { HarmonyConfigPluginError } from './errors';
10
+ export type { HarmonyConfigPluginErrorOptions } from './errors';
4
11
  export { atomicWrite, stableJson as stableHarmonyJson } from './files';
5
- export { HarmonySdkVersions, normalizeHarmonyConfig } from './normalizeConfig';
12
+ export { normalizeHarmonyConfig } from './normalizeConfig';
6
13
  export type { HarmonyExpoConfig, NormalizedHarmonyConfig } from './normalizeConfig';
7
- export { HarmonyModNames as HARMONY_MOD_NAMES, recordManagedFile, withAppJson, withArkTSPackageProvider, withCMakeLists, withCngManifest, withColors, withCppPackageProvider, withEntryAbility, withEntryBuildProfile, withEntryHvigor, withEntryOhPackage, withHarmonyAutolinking, withHarmonyBaseMods, withHarmonyDangerousMod, withHarmonyMod, withHarmonyResources, withHvigorConfig, withIndexPage, withMedia, withModuleJson, withNativeInputsStamp, withProfiles, withProjectBuildProfile, withReactNativeConfig, withRootHvigor, withRootOhPackage, withStrings, withWorker, } from './mods';
8
- export type { HarmonyJson, HarmonyMediaDescriptor, HarmonyMediaMap, HarmonyModAction, HarmonyModName, HarmonyResourceMap, } from './mods';
9
- export { getHarmonyConfigPlugins, normalizeHarmonyConfigPlugins, registerHarmonyConfigPlugin, } from './ownership';
14
+ export { HarmonyModNames as HARMONY_MOD_NAMES, recordManagedFile, withAppJson, withArkTSPackageProvider, withCMakeLists, withColors, withCppPackageProvider, withAbilityStage, withEntryAbility, withEntryBuildProfile, withEntryHvigor, withEntryOhPackage, withHarmonyAutolinking, withHarmonyBaseMods, withHarmonyDangerousMod, withHarmonyMod, withHarmonyResources, withHvigorConfig, withIndexPage, withMedia, withModuleJson, withProfiles, withProjectBuildProfile, withReactNativeConfig, withRootHvigor, withRootOhPackage, withStrings, withWorker, } from './mods';
15
+ export type { HarmonyJson, HarmonyMediaDescriptor, HarmonyMediaMap, HarmonyModName, HarmonyResourceMap, } from './mods';
16
+ export { registerHarmonyConfigPlugin } from './ownership';
10
17
  export type { HarmonyConfigPluginOwnership } from './ownership';
11
18
  export { HarmonyPaths } from './paths';
12
19
  export type { HarmonyManagedPaths, HarmonyPathsApi, HarmonyProjectPathCandidates, HarmonyProjectPaths, HarmonyResourcePaths, } from './paths';
20
+ export type { HarmonyConfigPlugin, HarmonyModAction, HarmonyModConfig } from './pluginTypes';
21
+ export * as HarmonyManifest from './manifest';
22
+ export * as HarmonyResources from './resources';
23
+ export type { HarmonyAbility, HarmonyExtensionAbility, HarmonyMetadata, HarmonyModule, HarmonyModuleJson } from './manifest';
24
+ export type { HarmonyResourceFile, HarmonyResourceItem } from './resources';
25
+ export { withHarmonyGeneratedFiles, withRawfile } from './generatedFiles';
26
+ export type { HarmonyFileDescriptor, HarmonyFileMap, HarmonyGeneratedFilesOptions } from './generatedFiles';
27
+ export { HarmonyPermissions, withHarmonyPermissions } from './permissions';