@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.
- package/README.md +203 -11
- package/build/compiler.d.ts +7 -0
- package/build/compiler.js +12 -0
- package/build/composition.d.ts +24 -0
- package/build/composition.js +11 -0
- package/build/config.d.ts +51 -0
- package/build/config.js +6 -0
- package/build/customFiles.d.ts +15 -0
- package/build/customFiles.js +108 -0
- package/build/errors.d.ts +11 -0
- package/build/errors.js +14 -0
- package/build/files.d.ts +6 -0
- package/build/files.js +69 -0
- package/build/generatedFiles.d.ts +15 -0
- package/build/generatedFiles.js +70 -0
- package/build/index.d.ts +27 -0
- package/build/index.js +52 -6
- package/build/internal.d.ts +3 -0
- package/build/internal.js +13 -0
- package/build/manifest.d.ts +38 -0
- package/build/manifest.js +71 -0
- package/build/mods.d.ts +57 -0
- package/build/mods.js +428 -0
- package/build/nativeInputs.d.ts +14 -0
- package/build/nativeInputs.js +81 -0
- package/build/normalizeConfig.d.ts +35 -0
- package/build/normalizeConfig.js +270 -0
- package/build/ownership.d.ts +17 -0
- package/build/ownership.js +129 -0
- package/build/paths.d.ts +64 -0
- package/build/paths.js +129 -0
- package/build/permissions.d.ts +12 -0
- package/build/permissions.js +60 -0
- package/build/pluginTypes.d.ts +15 -0
- package/build/pluginTypes.js +2 -0
- package/build/resources.d.ts +13 -0
- package/build/resources.js +55 -0
- package/package.json +9 -9
- package/src/compiler.ts +21 -0
- package/src/composition.ts +45 -0
- package/src/config.ts +3 -19
- package/src/customFiles.ts +142 -0
- package/src/files.ts +3 -4
- package/src/generatedFiles.ts +85 -0
- package/src/index.ts +19 -11
- package/src/internal.ts +3 -0
- package/src/manifest.ts +151 -0
- package/src/mods.ts +170 -129
- package/src/nativeInputs.ts +0 -1
- package/src/normalizeConfig.ts +51 -59
- package/src/ownership.ts +25 -5
- package/src/paths.ts +38 -46
- package/src/permissions.ts +68 -0
- package/src/pluginTypes.ts +24 -0
- 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
|
-
|
|
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`
|
|
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
|
-
|
|
32
|
+
`defineExpoHarmonyConfig` 里的 `harmony` 字段可以写这些配置:
|
|
33
33
|
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
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 [@
|
|
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
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ExpoConfig } from '@expo/config-types';
|
|
2
|
+
export type HarmonyPlatform = 'harmony';
|
|
3
|
+
export type ExpoKnownPlatform = NonNullable<ExpoConfig['platforms']>[number];
|
|
4
|
+
export type ExpoHarmonyPlatform = ExpoKnownPlatform | HarmonyPlatform;
|
|
5
|
+
export type HarmonyDeviceType = 'phone' | 'tablet' | '2in1';
|
|
6
|
+
export type HarmonyOrientation = 'default' | 'portrait' | 'landscape' | 'portrait_inverted' | 'landscape_inverted' | 'auto_rotation';
|
|
7
|
+
export interface HarmonyPermission {
|
|
8
|
+
name: string;
|
|
9
|
+
reason?: string;
|
|
10
|
+
usedScene?: {
|
|
11
|
+
abilities?: string[];
|
|
12
|
+
when?: 'inuse' | 'always';
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export type HarmonySkillUri = Record<string, string | number | boolean>;
|
|
16
|
+
export interface HarmonySkill {
|
|
17
|
+
entities?: string[];
|
|
18
|
+
actions?: string[];
|
|
19
|
+
uris?: HarmonySkillUri[];
|
|
20
|
+
}
|
|
21
|
+
export interface HarmonyConfig {
|
|
22
|
+
bundleName: string;
|
|
23
|
+
moduleName?: string;
|
|
24
|
+
abilityName?: string;
|
|
25
|
+
productName?: string;
|
|
26
|
+
vendor?: string;
|
|
27
|
+
versionCode?: number;
|
|
28
|
+
versionName?: string;
|
|
29
|
+
targetApiVersion?: number;
|
|
30
|
+
targetSdkVersion?: number | string;
|
|
31
|
+
compatibleSdkVersion?: number | string;
|
|
32
|
+
deviceTypes?: HarmonyDeviceType[];
|
|
33
|
+
permissions?: HarmonyPermission[];
|
|
34
|
+
skills?: HarmonySkill[];
|
|
35
|
+
querySchemes?: string[];
|
|
36
|
+
icon?: string;
|
|
37
|
+
label?: string;
|
|
38
|
+
backgroundColor?: string;
|
|
39
|
+
orientation?: HarmonyOrientation;
|
|
40
|
+
userInterfaceStyle?: 'light' | 'dark' | 'automatic';
|
|
41
|
+
jsEngine?: 'hermes';
|
|
42
|
+
abiFilters?: string[];
|
|
43
|
+
signingConfigFile?: string;
|
|
44
|
+
}
|
|
45
|
+
export type ExpoConfigWithHarmony = Omit<ExpoConfig, 'platforms'> & {
|
|
46
|
+
platforms?: ExpoHarmonyPlatform[];
|
|
47
|
+
harmony?: HarmonyConfig;
|
|
48
|
+
};
|
|
49
|
+
export declare function defineExpoHarmonyConfig<T extends {
|
|
50
|
+
expo: ExpoConfigWithHarmony;
|
|
51
|
+
} | ExpoConfigWithHarmony>(config: T): T;
|
package/build/config.js
ADDED
|
@@ -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;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface HarmonyConfigPluginErrorOptions {
|
|
2
|
+
cause?: unknown;
|
|
3
|
+
file?: string;
|
|
4
|
+
operation?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class HarmonyConfigPluginError extends Error {
|
|
7
|
+
readonly code: string;
|
|
8
|
+
readonly operation: string;
|
|
9
|
+
readonly file?: string;
|
|
10
|
+
constructor(code: string, message: string, options?: HarmonyConfigPluginErrorOptions);
|
|
11
|
+
}
|
package/build/errors.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HarmonyConfigPluginError = void 0;
|
|
4
|
+
class HarmonyConfigPluginError extends Error {
|
|
5
|
+
constructor(code, message, options = {}) {
|
|
6
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
7
|
+
this.name = 'HarmonyConfigPluginError';
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.operation = options.operation || 'config-plugin';
|
|
10
|
+
if (options.file)
|
|
11
|
+
this.file = options.file;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.HarmonyConfigPluginError = HarmonyConfigPluginError;
|
package/build/files.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function stableJson(value: unknown): string;
|
|
2
|
+
export declare function readJson5<T>(file: string, fallback: T, name: string): Promise<T>;
|
|
3
|
+
export declare function readText(file: string, fallback?: string): Promise<string>;
|
|
4
|
+
export declare function atomicWrite(file: string, content: string | Uint8Array): Promise<void>;
|
|
5
|
+
export declare function writeJson5(file: string, value: unknown): Promise<void>;
|
|
6
|
+
export declare function sha256File(file: string): Promise<string>;
|
package/build/files.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
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.stableJson = stableJson;
|
|
7
|
+
exports.readJson5 = readJson5;
|
|
8
|
+
exports.readText = readText;
|
|
9
|
+
exports.atomicWrite = atomicWrite;
|
|
10
|
+
exports.writeJson5 = writeJson5;
|
|
11
|
+
exports.sha256File = sha256File;
|
|
12
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
13
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
15
|
+
const json5_1 = __importDefault(require("json5"));
|
|
16
|
+
const errors_1 = require("./errors");
|
|
17
|
+
function sortValue(value) {
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return value.map(sortValue);
|
|
20
|
+
if (value && typeof value === 'object' && !Buffer.isBuffer(value)) {
|
|
21
|
+
const record = value;
|
|
22
|
+
const entries = Object.keys(record).sort().map(key => [key, sortValue(record[key])]);
|
|
23
|
+
return Object.fromEntries(entries);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function stableJson(value) {
|
|
28
|
+
return `${JSON.stringify(sortValue(value), null, 2)}\n`;
|
|
29
|
+
}
|
|
30
|
+
async function readJson5(file, fallback, name) {
|
|
31
|
+
try {
|
|
32
|
+
const source = await node_fs_1.default.promises.readFile(file, 'utf8');
|
|
33
|
+
return json5_1.default.parse(source);
|
|
34
|
+
}
|
|
35
|
+
catch (cause) {
|
|
36
|
+
if (cause.code === 'ENOENT')
|
|
37
|
+
return structuredClone(fallback);
|
|
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
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function readText(file, fallback = '') {
|
|
42
|
+
try {
|
|
43
|
+
return await node_fs_1.default.promises.readFile(file, 'utf8');
|
|
44
|
+
}
|
|
45
|
+
catch (cause) {
|
|
46
|
+
if (cause.code === 'ENOENT')
|
|
47
|
+
return fallback;
|
|
48
|
+
throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_TEXT_INVALID', `Cannot read ${file}: ${cause.message}`, { cause, file, operation: 'harmony.mod.read' });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function atomicWrite(file, content) {
|
|
52
|
+
const directory = node_path_1.default.dirname(file);
|
|
53
|
+
const temporary = node_path_1.default.join(directory, `.${node_path_1.default.basename(file)}.${process.pid}.${node_crypto_1.default.randomBytes(6).toString('hex')}.tmp`);
|
|
54
|
+
await node_fs_1.default.promises.mkdir(directory, { recursive: true });
|
|
55
|
+
try {
|
|
56
|
+
await node_fs_1.default.promises.writeFile(temporary, content);
|
|
57
|
+
await node_fs_1.default.promises.rename(temporary, file);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await node_fs_1.default.promises.rm(temporary, { force: true }).catch(() => { });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function writeJson5(file, value) {
|
|
64
|
+
await atomicWrite(file, stableJson(value));
|
|
65
|
+
}
|
|
66
|
+
async function sha256File(file) {
|
|
67
|
+
const data = Uint8Array.from(await node_fs_1.default.promises.readFile(file));
|
|
68
|
+
return node_crypto_1.default.createHash('sha256').update(data).digest('hex');
|
|
69
|
+
}
|
|
@@ -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>;
|