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

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 CHANGED
@@ -40,9 +40,9 @@ module.exports = defineExpoHarmonyConfig({
40
40
  | `vendor` | `expo-harmony` |
41
41
  | `versionCode` | `1` |
42
42
  | `versionName` | 配置里的 `version`,没有则 `1.0.0` |
43
- | `targetApiVersion` | `24` |
44
- | `targetSdkVersion` | 数字或 SDK 标签,默认跟随 `targetApiVersion` |
45
- | `compatibleSdkVersion` | `13`,不能低于 13,也不能大于 `targetApiVersion` |
43
+ | `targetApiVersion` | 旧版别名,建议改用 `expo-build-properties` 的 `targetSdkVersion` |
44
+ | `targetSdkVersion` | 数字或 SDK 字符串,默认 `24`,兼容旧 `targetApiVersion` |
45
+ | `compatibleSdkVersion` | `20`,不能低于 20,也不能大于 target |
46
46
  | `deviceTypes` | `['phone', 'tablet']` |
47
47
  | `permissions` | 权限列表,默认包含 `ohos.permission.INTERNET` |
48
48
  | `skills` | 技能列表,`scheme` 会追加对应项 |
@@ -56,7 +56,7 @@ module.exports = defineExpoHarmonyConfig({
56
56
  | `abiFilters` | `['arm64-v8a', 'x86_64']` |
57
57
  | `signingConfigFile` | 签名配置文件路径,不设置则忽略 |
58
58
 
59
- `targetSdkVersion` 和 `compatibleSdkVersion` 可以写数字或带 API 级别的 SDK 标签。内置标签覆盖 API 13、14、20、21、23、24,其他版本写成 `6.1.1(24)` 这样的形式。`backgroundColor` 写 `#RRGGBB` 或 `#RRGGBBAA`,写入原生工程时转成 ARGB。`jsEngine` 目前只能是 `hermes`。
59
+ SDK/ABI 的这些旧字段继续兼容,新项目建议使用 `@expo-harmony/expo-build-properties`。`targetSdkVersion` 和 `compatibleSdkVersion` 可以写数字或带 API 级别的 SDK 标签;API 26 起使用 `26.0.0` 这样的分段版本。内置旧标签覆盖 API 13、14、20、21、23、24,已知 API 不接受错误的标签。`targetApiVersion` 与 `targetSdkVersion` 同时存在时须一致。`backgroundColor` 写 `#RRGGBB` 或 `#RRGGBBAA`,写入原生工程时转成 ARGB。`jsEngine` 目前只能是 `hermes`。
60
60
 
61
61
  `normalizeHarmonyConfig(config)` 返回补齐默认值并校验后的配置。
62
62
 
package/build/config.d.ts CHANGED
@@ -26,6 +26,10 @@ export interface HarmonyConfig {
26
26
  vendor?: string;
27
27
  versionCode?: number;
28
28
  versionName?: string;
29
+ runtimeVersion?: string | {
30
+ policy: 'nativeVersion' | 'sdkVersion' | 'appVersion' | 'fingerprint';
31
+ };
32
+ /** @deprecated Configure targetSdkVersion in @expo-harmony/expo-build-properties. */
29
33
  targetApiVersion?: number;
30
34
  targetSdkVersion?: number | string;
31
35
  compatibleSdkVersion?: number | string;
@@ -41,6 +45,8 @@ export interface HarmonyConfig {
41
45
  jsEngine?: 'hermes';
42
46
  abiFilters?: string[];
43
47
  signingConfigFile?: string;
48
+ /** Native module build options supplied by config plugins. */
49
+ moduleBuilds?: Record<string, Record<string, string | number | boolean>>;
44
50
  }
45
51
  export type ExpoConfigWithHarmony = Omit<ExpoConfig, 'platforms'> & {
46
52
  platforms?: ExpoHarmonyPlatform[];
package/build/index.d.ts CHANGED
@@ -25,3 +25,5 @@ export type { HarmonyResourceFile, HarmonyResourceItem } from './resources';
25
25
  export { withHarmonyGeneratedFiles, withRawfile } from './generatedFiles';
26
26
  export type { HarmonyFileDescriptor, HarmonyFileMap, HarmonyGeneratedFilesOptions } from './generatedFiles';
27
27
  export { HarmonyPermissions, withHarmonyPermissions } from './permissions';
28
+ export { parseHarmonySdkVersion, compareHarmonyApiVersions } from './sdkVersion';
29
+ export type { HarmonySdkVersion } from './sdkVersion';
package/build/index.js CHANGED
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.withHarmonyPermissions = exports.HarmonyPermissions = exports.withRawfile = exports.withHarmonyGeneratedFiles = exports.HarmonyResources = exports.HarmonyManifest = exports.HarmonyPaths = exports.registerHarmonyConfigPlugin = exports.withWorker = exports.withStrings = exports.withRootOhPackage = exports.withRootHvigor = exports.withReactNativeConfig = exports.withProjectBuildProfile = exports.withProfiles = exports.withModuleJson = exports.withMedia = exports.withIndexPage = exports.withHvigorConfig = exports.withHarmonyResources = exports.withHarmonyMod = exports.withHarmonyDangerousMod = exports.withHarmonyBaseMods = exports.withHarmonyAutolinking = exports.withEntryOhPackage = exports.withEntryHvigor = exports.withEntryBuildProfile = exports.withEntryAbility = exports.withAbilityStage = exports.withCppPackageProvider = exports.withColors = exports.withCMakeLists = exports.withArkTSPackageProvider = exports.withAppJson = exports.recordManagedFile = exports.HARMONY_MOD_NAMES = exports.normalizeHarmonyConfig = exports.stableHarmonyJson = exports.atomicWrite = exports.HarmonyConfigPluginError = exports.withHarmonyJsonFile = exports.createHarmonyFileMod = exports.compileHarmonyModsAsync = exports.withStaticPlugin = exports.withRunOnce = exports.withPlugins = exports.createRunOncePlugin = exports.defineExpoHarmonyConfig = void 0;
36
+ exports.compareHarmonyApiVersions = exports.parseHarmonySdkVersion = exports.withHarmonyPermissions = exports.HarmonyPermissions = exports.withRawfile = exports.withHarmonyGeneratedFiles = exports.HarmonyResources = exports.HarmonyManifest = exports.HarmonyPaths = exports.registerHarmonyConfigPlugin = exports.withWorker = exports.withStrings = exports.withRootOhPackage = exports.withRootHvigor = exports.withReactNativeConfig = exports.withProjectBuildProfile = exports.withProfiles = exports.withModuleJson = exports.withMedia = exports.withIndexPage = exports.withHvigorConfig = exports.withHarmonyResources = exports.withHarmonyMod = exports.withHarmonyDangerousMod = exports.withHarmonyBaseMods = exports.withHarmonyAutolinking = exports.withEntryOhPackage = exports.withEntryHvigor = exports.withEntryBuildProfile = exports.withEntryAbility = exports.withAbilityStage = exports.withCppPackageProvider = exports.withColors = exports.withCMakeLists = exports.withArkTSPackageProvider = exports.withAppJson = exports.recordManagedFile = exports.HARMONY_MOD_NAMES = exports.normalizeHarmonyConfig = exports.stableHarmonyJson = exports.atomicWrite = exports.HarmonyConfigPluginError = exports.withHarmonyJsonFile = exports.createHarmonyFileMod = exports.compileHarmonyModsAsync = exports.withStaticPlugin = exports.withRunOnce = exports.withPlugins = exports.createRunOncePlugin = exports.defineExpoHarmonyConfig = void 0;
37
37
  var config_1 = require("./config");
38
38
  Object.defineProperty(exports, "defineExpoHarmonyConfig", { enumerable: true, get: function () { return config_1.defineExpoHarmonyConfig; } });
39
39
  var composition_1 = require("./composition");
@@ -94,3 +94,6 @@ Object.defineProperty(exports, "withRawfile", { enumerable: true, get: function
94
94
  var permissions_1 = require("./permissions");
95
95
  Object.defineProperty(exports, "HarmonyPermissions", { enumerable: true, get: function () { return permissions_1.HarmonyPermissions; } });
96
96
  Object.defineProperty(exports, "withHarmonyPermissions", { enumerable: true, get: function () { return permissions_1.withHarmonyPermissions; } });
97
+ var sdkVersion_1 = require("./sdkVersion");
98
+ Object.defineProperty(exports, "parseHarmonySdkVersion", { enumerable: true, get: function () { return sdkVersion_1.parseHarmonySdkVersion; } });
99
+ Object.defineProperty(exports, "compareHarmonyApiVersions", { enumerable: true, get: function () { return sdkVersion_1.compareHarmonyApiVersions; } });
@@ -10,13 +10,14 @@ interface NormalizedHarmonyConfig {
10
10
  icon?: string;
11
11
  label: string;
12
12
  moduleName: string;
13
+ moduleBuilds?: NonNullable<HarmonyConfig['moduleBuilds']>;
13
14
  nativeOrientation: Exclude<NonNullable<HarmonyConfig['orientation']>, 'default'> | 'unspecified';
14
15
  permissions: NonNullable<HarmonyConfig['permissions']>;
15
16
  productName: string;
16
17
  querySchemes: string[];
17
18
  signingConfigFile?: string;
18
19
  skills: NonNullable<HarmonyConfig['skills']>;
19
- targetApiVersion: number;
20
+ targetApiVersion: number | string;
20
21
  targetSdkVersionString: string;
21
22
  vendor: string;
22
23
  versionCode: number;
@@ -4,6 +4,7 @@ exports.normalizePermission = normalizePermission;
4
4
  exports.normalizeHarmonyConfig = normalizeHarmonyConfig;
5
5
  const errors_1 = require("./errors");
6
6
  const resources_1 = require("./resources");
7
+ const sdkVersion_1 = require("./sdkVersion");
7
8
  const BundlePattern = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*){2,}$/;
8
9
  const IdentifierPattern = /^[A-Za-z][A-Za-z0-9_]*$/;
9
10
  const ColorPattern = /^(#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{8})$/;
@@ -18,14 +19,6 @@ const Orientations = new Set([
18
19
  const DeviceTypes = new Set(['phone', 'tablet', '2in1']);
19
20
  const MinimumApi = 20;
20
21
  const TargetApi = 24;
21
- const SdkVersions = new Map([
22
- [13, '5.0.1(13)'],
23
- [14, '5.0.2(14)'],
24
- [20, '6.0.0(20)'],
25
- [21, '6.0.1(21)'],
26
- [23, '6.1.0(23)'],
27
- [24, '6.1.1(24)'],
28
- ]);
29
22
  class HarmonyConfigError extends errors_1.HarmonyConfigPluginError {
30
23
  constructor(message) {
31
24
  super('ERR_HARMONY_CONFIG_INVALID', message, { operation: 'normalize-config' });
@@ -56,43 +49,6 @@ function normalizeColor(value, field, fallback) {
56
49
  }
57
50
  return (0, resources_1.toArgb)(color);
58
51
  }
59
- function parseSdkApi(value, field) {
60
- if (value === undefined)
61
- return null;
62
- if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0)
63
- return value;
64
- if (typeof value !== 'string') {
65
- throw new HarmonyConfigError(`harmony.${field} must be a positive API number or an SDK label.`);
66
- }
67
- const match = /\((\d+)\)$/u.exec(value.trim());
68
- const api = Number(match?.[1]);
69
- if (!match || !Number.isSafeInteger(api) || api <= 0) {
70
- throw new HarmonyConfigError(`harmony.${field} must end with its API level in parentheses, for example "6.0.0(20)".`);
71
- }
72
- return api;
73
- }
74
- function resolveSdkVersion(api, field, value) {
75
- if (value !== undefined) {
76
- if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) {
77
- if (value !== api) {
78
- throw new HarmonyConfigError(`harmony.${field} is API ${value}, but the configured API level is ${api}.`);
79
- }
80
- }
81
- else {
82
- const version = readString(value, `harmony.${field}`);
83
- const parsed = parseSdkApi(version, field);
84
- if (parsed !== api) {
85
- throw new HarmonyConfigError(`harmony.${field} describes API ${parsed}, but the configured API level is ${api}.`);
86
- }
87
- return version;
88
- }
89
- }
90
- const version = SdkVersions.get(api);
91
- if (!version) {
92
- throw new HarmonyConfigError(`Harmony API ${api} has no built-in SDK label. Set harmony.${field} explicitly so new SDK releases do not require a package update.`);
93
- }
94
- return version;
95
- }
96
52
  function normalizeStringArray(value, field, allowed) {
97
53
  if (!Array.isArray(value) || value.length === 0) {
98
54
  throw new HarmonyConfigError(`${field} must be a non-empty array.`);
@@ -190,13 +146,19 @@ function normalizeHarmonyConfig(config) {
190
146
  }
191
147
  const version = readString(harmony.versionName, 'harmony.versionName', config.version || '1.0.0');
192
148
  const code = readPositiveInteger(harmony.versionCode, 'versionCode', 1);
193
- const target = readPositiveInteger(harmony.targetApiVersion, 'targetApiVersion', parseSdkApi(harmony.targetSdkVersion, 'targetSdkVersion') ?? TargetApi);
194
- const compatible = readPositiveInteger(parseSdkApi(harmony.compatibleSdkVersion, 'compatibleSdkVersion') ?? undefined, 'compatibleSdkVersion', MinimumApi);
195
- if (compatible < MinimumApi) {
149
+ const target = (0, sdkVersion_1.parseHarmonySdkVersion)(harmony.targetSdkVersion ?? harmony.targetApiVersion ?? TargetApi, 'harmony.targetSdkVersion');
150
+ if (harmony.targetApiVersion !== undefined) {
151
+ const alias = (0, sdkVersion_1.parseHarmonySdkVersion)(harmony.targetApiVersion, 'harmony.targetApiVersion');
152
+ if ((0, sdkVersion_1.compareHarmonyApiVersions)(alias.api, target.api) !== 0) {
153
+ throw new HarmonyConfigError('harmony.targetApiVersion conflicts with harmony.targetSdkVersion.');
154
+ }
155
+ }
156
+ const compatible = (0, sdkVersion_1.parseHarmonySdkVersion)(harmony.compatibleSdkVersion ?? MinimumApi, 'harmony.compatibleSdkVersion');
157
+ if ((0, sdkVersion_1.compareHarmonyApiVersions)(compatible.api, MinimumApi) < 0) {
196
158
  throw new HarmonyConfigError(`Harmony compatible API must be ${MinimumApi} or newer.`);
197
159
  }
198
- if (compatible > target) {
199
- throw new HarmonyConfigError('Harmony compatibleSdkVersion cannot exceed targetApiVersion.');
160
+ if ((0, sdkVersion_1.compareHarmonyApiVersions)(compatible.api, target.api) > 0) {
161
+ throw new HarmonyConfigError('Harmony compatibleSdkVersion cannot exceed targetSdkVersion.');
200
162
  }
201
163
  const orientation = harmony.orientation || config.orientation || 'default';
202
164
  if (!Orientations.has(orientation)) {
@@ -211,7 +173,7 @@ function normalizeHarmonyConfig(config) {
211
173
  ? ['arm64-v8a', 'x86_64']
212
174
  : normalizeStringArray(harmony.abiFilters, 'harmony.abiFilters');
213
175
  for (const abi of abis) {
214
- if (!/^[A-Za-z0-9_-]+$/.test(abi)) {
176
+ if (!['arm64-v8a', 'x86_64'].includes(abi)) {
215
177
  throw new HarmonyConfigError(`Invalid Harmony ABI filter: ${abi}`);
216
178
  }
217
179
  }
@@ -249,12 +211,27 @@ function normalizeHarmonyConfig(config) {
249
211
  const devices = harmony.deviceTypes === undefined
250
212
  ? ['phone', 'tablet']
251
213
  : normalizeStringArray(harmony.deviceTypes, 'harmony.deviceTypes', DeviceTypes);
214
+ const moduleBuilds = harmony.moduleBuilds;
215
+ if (moduleBuilds !== undefined) {
216
+ if (!moduleBuilds || typeof moduleBuilds !== 'object' || Array.isArray(moduleBuilds)) {
217
+ throw new HarmonyConfigError('harmony.moduleBuilds must be an object.');
218
+ }
219
+ for (const [name, options] of Object.entries(moduleBuilds)) {
220
+ if (!/^(@[a-z0-9._~-]+\/)?[a-z0-9._~-]+$/u.test(name)
221
+ || !options || typeof options !== 'object' || Array.isArray(options)
222
+ || Object.values(options).some(value => !['boolean', 'string', 'number'].includes(typeof value)
223
+ || (typeof value === 'number' && !Number.isFinite(value)))) {
224
+ throw new HarmonyConfigError(`Invalid native module build options for ${name}.`);
225
+ }
226
+ }
227
+ }
252
228
  return Object.freeze({
229
+ ...(moduleBuilds && Object.keys(moduleBuilds).length ? { moduleBuilds: structuredClone(moduleBuilds) } : {}),
253
230
  abiFilters: abis,
254
231
  abilityName: ability,
255
232
  backgroundColor: background,
256
233
  bundleName: bundle,
257
- compatibleSdkVersionString: resolveSdkVersion(compatible, 'compatibleSdkVersion', harmony.compatibleSdkVersion),
234
+ compatibleSdkVersionString: compatible.native,
258
235
  deviceTypes: devices,
259
236
  icon: harmony.icon || config.icon,
260
237
  label: readString(harmony.label, 'harmony.label', config.name),
@@ -265,8 +242,8 @@ function normalizeHarmonyConfig(config) {
265
242
  querySchemes: queries,
266
243
  signingConfigFile: signing,
267
244
  skills,
268
- targetApiVersion: target,
269
- targetSdkVersionString: resolveSdkVersion(target, 'targetSdkVersion', harmony.targetSdkVersion),
245
+ targetApiVersion: target.api,
246
+ targetSdkVersionString: target.native,
270
247
  vendor: readString(harmony.vendor, 'harmony.vendor', 'expo-harmony'),
271
248
  versionCode: code,
272
249
  versionName: version,
@@ -0,0 +1,6 @@
1
+ export interface HarmonySdkVersion {
2
+ api: number | string;
3
+ native: string;
4
+ }
5
+ export declare function parseHarmonySdkVersion(value: unknown, field?: string): HarmonySdkVersion;
6
+ export declare function compareHarmonyApiVersions(left: number | string, right: number | string): number;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseHarmonySdkVersion = parseHarmonySdkVersion;
4
+ exports.compareHarmonyApiVersions = compareHarmonyApiVersions;
5
+ const errors_1 = require("./errors");
6
+ const Labels = new Map([
7
+ [13, '5.0.1(13)'], [14, '5.0.2(14)'], [20, '6.0.0(20)'],
8
+ [21, '6.0.1(21)'], [23, '6.1.0(23)'], [24, '6.1.1(24)'],
9
+ ]);
10
+ function parseHarmonySdkVersion(value, field = 'SDK version') {
11
+ if (typeof value === 'number') {
12
+ if (!Number.isSafeInteger(value) || value < 1 || value > 999) {
13
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `${field}: expected a positive API level.`);
14
+ }
15
+ if (value >= 26)
16
+ return { api: `${value}.0.0`, native: `${value}.0.0` };
17
+ const native = Labels.get(value);
18
+ if (!native) {
19
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `${field}: API ${value} requires an explicit HarmonyOS SDK label.`);
20
+ }
21
+ return { api: value, native };
22
+ }
23
+ if (typeof value !== 'string') {
24
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `${field}: expected an API number or HarmonyOS SDK version.`);
25
+ }
26
+ const version = value.trim();
27
+ const dotted = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(version);
28
+ if (dotted && Number(dotted[1]) >= 26
29
+ && dotted.slice(1).every(part => String(Number(part)) === part)) {
30
+ return { api: version, native: version };
31
+ }
32
+ const label = /^\d+\.\d+\.\d+\(([1-9]\d*)\)$/.exec(version);
33
+ if (label && Number(label[1]) < 26) {
34
+ const api = Number(label[1]);
35
+ const known = Labels.get(api);
36
+ if (known && known !== version) {
37
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `${field}: API ${api} uses SDK label ${known}.`);
38
+ }
39
+ return { api, native: version };
40
+ }
41
+ throw new errors_1.HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `${field}: expected an SDK label such as "6.1.1(24)", or a dotted API version such as "26.0.0".`);
42
+ }
43
+ function compareHarmonyApiVersions(left, right) {
44
+ const a = String(left).split('.').map(Number);
45
+ const b = String(right).split('.').map(Number);
46
+ for (let i = 0; i < 3; i++) {
47
+ const delta = (a[i] ?? 0) - (b[i] ?? 0);
48
+ if (delta)
49
+ return delta;
50
+ }
51
+ return 0;
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo-harmony/config-plugins",
3
- "version": "55.0.10-harmony.4",
3
+ "version": "55.0.11-harmony.2",
4
4
  "keywords": [
5
5
  "react-native",
6
6
  "expo",
@@ -42,13 +42,10 @@
42
42
  }
43
43
  },
44
44
  "dependencies": {
45
- "@expo/config-plugins": "55.0.10",
45
+ "@expo/config-plugins": "55.0.11",
46
46
  "@expo/config-types": "^55.0.5",
47
47
  "json5": "2.2.3"
48
48
  },
49
- "peerDependencies": {
50
- "@expo/config": "55.0.17"
51
- },
52
49
  "devDependencies": {
53
50
  "@types/node": "18.19.0",
54
51
  "typescript": "^6.0.3"
package/src/config.ts CHANGED
@@ -34,6 +34,10 @@ export interface HarmonyConfig {
34
34
  vendor?: string;
35
35
  versionCode?: number;
36
36
  versionName?: string;
37
+ runtimeVersion?: string | {
38
+ policy: 'nativeVersion' | 'sdkVersion' | 'appVersion' | 'fingerprint';
39
+ };
40
+ /** @deprecated Configure targetSdkVersion in @expo-harmony/expo-build-properties. */
37
41
  targetApiVersion?: number;
38
42
  targetSdkVersion?: number | string;
39
43
  compatibleSdkVersion?: number | string;
@@ -49,6 +53,8 @@ export interface HarmonyConfig {
49
53
  jsEngine?: 'hermes';
50
54
  abiFilters?: string[];
51
55
  signingConfigFile?: string;
56
+ /** Native module build options supplied by config plugins. */
57
+ moduleBuilds?: Record<string, Record<string, string | number | boolean>>;
52
58
  }
53
59
 
54
60
  export type ExpoConfigWithHarmony = Omit<ExpoConfig, 'platforms'> & {
package/src/index.ts CHANGED
@@ -78,3 +78,6 @@ export type { HarmonyResourceFile, HarmonyResourceItem } from './resources';
78
78
  export { withHarmonyGeneratedFiles, withRawfile } from './generatedFiles';
79
79
  export type { HarmonyFileDescriptor, HarmonyFileMap, HarmonyGeneratedFilesOptions } from './generatedFiles';
80
80
  export { HarmonyPermissions, withHarmonyPermissions } from './permissions';
81
+
82
+ export { parseHarmonySdkVersion, compareHarmonyApiVersions } from './sdkVersion';
83
+ export type { HarmonySdkVersion } from './sdkVersion';
@@ -7,6 +7,7 @@ import type {
7
7
  } from './config';
8
8
  import { HarmonyConfigPluginError } from './errors';
9
9
  import { toArgb } from './resources';
10
+ import { compareHarmonyApiVersions, parseHarmonySdkVersion } from './sdkVersion';
10
11
 
11
12
  type HarmonyExpoConfig = ExpoConfigWithHarmony;
12
13
 
@@ -20,13 +21,14 @@ interface NormalizedHarmonyConfig {
20
21
  icon?: string;
21
22
  label: string;
22
23
  moduleName: string;
24
+ moduleBuilds?: NonNullable<HarmonyConfig['moduleBuilds']>;
23
25
  nativeOrientation: Exclude<NonNullable<HarmonyConfig['orientation']>, 'default'> | 'unspecified';
24
26
  permissions: NonNullable<HarmonyConfig['permissions']>;
25
27
  productName: string;
26
28
  querySchemes: string[];
27
29
  signingConfigFile?: string;
28
30
  skills: NonNullable<HarmonyConfig['skills']>;
29
- targetApiVersion: number;
31
+ targetApiVersion: number | string;
30
32
  targetSdkVersionString: string;
31
33
  vendor: string;
32
34
  versionCode: number;
@@ -47,14 +49,6 @@ const Orientations = new Set([
47
49
  const DeviceTypes = new Set<HarmonyDeviceType>(['phone', 'tablet', '2in1']);
48
50
  const MinimumApi = 20;
49
51
  const TargetApi = 24;
50
- const SdkVersions = new Map([
51
- [13, '5.0.1(13)'],
52
- [14, '5.0.2(14)'],
53
- [20, '6.0.0(20)'],
54
- [21, '6.0.1(21)'],
55
- [23, '6.1.0(23)'],
56
- [24, '6.1.1(24)'],
57
- ]);
58
52
 
59
53
  class HarmonyConfigError extends HarmonyConfigPluginError {
60
54
  constructor(message: string) {
@@ -96,58 +90,6 @@ function normalizeColor(value: unknown, field: string, fallback: string): string
96
90
  return toArgb(color);
97
91
  }
98
92
 
99
- function parseSdkApi(value: unknown, field: string): number | null {
100
- if (value === undefined) return null;
101
- if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) return value;
102
- if (typeof value !== 'string') {
103
- throw new HarmonyConfigError(`harmony.${field} must be a positive API number or an SDK label.`);
104
- }
105
-
106
- const match = /\((\d+)\)$/u.exec(value.trim());
107
- const api = Number(match?.[1]);
108
-
109
- if (!match || !Number.isSafeInteger(api) || api <= 0) {
110
- throw new HarmonyConfigError(
111
- `harmony.${field} must end with its API level in parentheses, for example "6.0.0(20)".`
112
- );
113
- }
114
-
115
- return api;
116
- }
117
-
118
- function resolveSdkVersion(api: number, field: string, value?: unknown): string {
119
- if (value !== undefined) {
120
- if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) {
121
- if (value !== api) {
122
- throw new HarmonyConfigError(
123
- `harmony.${field} is API ${value}, but the configured API level is ${api}.`
124
- );
125
- }
126
- } else {
127
- const version = readString(value, `harmony.${field}`);
128
- const parsed = parseSdkApi(version, field);
129
-
130
- if (parsed !== api) {
131
- throw new HarmonyConfigError(
132
- `harmony.${field} describes API ${parsed}, but the configured API level is ${api}.`
133
- );
134
- }
135
-
136
- return version;
137
- }
138
- }
139
-
140
- const version = SdkVersions.get(api);
141
-
142
- if (!version) {
143
- throw new HarmonyConfigError(
144
- `Harmony API ${api} has no built-in SDK label. Set harmony.${field} explicitly so new SDK releases do not require a package update.`
145
- );
146
- }
147
-
148
- return version;
149
- }
150
-
151
93
  function normalizeStringArray<T extends string = string>(
152
94
  value: unknown,
153
95
  field: string,
@@ -286,24 +228,25 @@ function normalizeHarmonyConfig(config: HarmonyExpoConfig): NormalizedHarmonyCon
286
228
  const version = readString(harmony.versionName, 'harmony.versionName', config.version || '1.0.0');
287
229
  const code = readPositiveInteger(harmony.versionCode, 'versionCode', 1);
288
230
 
289
- const target = readPositiveInteger(
290
- harmony.targetApiVersion,
291
- 'targetApiVersion',
292
- parseSdkApi(harmony.targetSdkVersion, 'targetSdkVersion') ?? TargetApi
293
- );
294
- const compatible = readPositiveInteger(
295
- parseSdkApi(harmony.compatibleSdkVersion, 'compatibleSdkVersion') ?? undefined,
296
- 'compatibleSdkVersion',
297
- MinimumApi
231
+ const target = parseHarmonySdkVersion(
232
+ harmony.targetSdkVersion ?? harmony.targetApiVersion ?? TargetApi, 'harmony.targetSdkVersion'
298
233
  );
299
234
 
300
- if (compatible < MinimumApi) {
301
- throw new HarmonyConfigError(
302
- `Harmony compatible API must be ${MinimumApi} or newer.`
303
- );
235
+ if (harmony.targetApiVersion !== undefined) {
236
+ const alias = parseHarmonySdkVersion(harmony.targetApiVersion, 'harmony.targetApiVersion');
237
+
238
+ if (compareHarmonyApiVersions(alias.api, target.api) !== 0) {
239
+ throw new HarmonyConfigError('harmony.targetApiVersion conflicts with harmony.targetSdkVersion.');
240
+ }
304
241
  }
305
- if (compatible > target) {
306
- throw new HarmonyConfigError('Harmony compatibleSdkVersion cannot exceed targetApiVersion.');
242
+
243
+ const compatible = parseHarmonySdkVersion(harmony.compatibleSdkVersion ?? MinimumApi, 'harmony.compatibleSdkVersion');
244
+
245
+ if (compareHarmonyApiVersions(compatible.api, MinimumApi) < 0) {
246
+ throw new HarmonyConfigError(`Harmony compatible API must be ${MinimumApi} or newer.`);
247
+ }
248
+ if (compareHarmonyApiVersions(compatible.api, target.api) > 0) {
249
+ throw new HarmonyConfigError('Harmony compatibleSdkVersion cannot exceed targetSdkVersion.');
307
250
  }
308
251
 
309
252
  const orientation = harmony.orientation || config.orientation || 'default';
@@ -327,7 +270,7 @@ function normalizeHarmonyConfig(config: HarmonyExpoConfig): NormalizedHarmonyCon
327
270
  : normalizeStringArray(harmony.abiFilters, 'harmony.abiFilters');
328
271
 
329
272
  for (const abi of abis) {
330
- if (!/^[A-Za-z0-9_-]+$/.test(abi)) {
273
+ if (!['arm64-v8a', 'x86_64'].includes(abi)) {
331
274
  throw new HarmonyConfigError(`Invalid Harmony ABI filter: ${abi}`);
332
275
  }
333
276
  }
@@ -378,16 +321,28 @@ function normalizeHarmonyConfig(config: HarmonyExpoConfig): NormalizedHarmonyCon
378
321
  DeviceTypes
379
322
  );
380
323
 
324
+ const moduleBuilds = harmony.moduleBuilds;
325
+ if (moduleBuilds !== undefined) {
326
+ if (!moduleBuilds || typeof moduleBuilds !== 'object' || Array.isArray(moduleBuilds)) {
327
+ throw new HarmonyConfigError('harmony.moduleBuilds must be an object.');
328
+ }
329
+ for (const [name, options] of Object.entries(moduleBuilds)) {
330
+ if (!/^(@[a-z0-9._~-]+\/)?[a-z0-9._~-]+$/u.test(name)
331
+ || !options || typeof options !== 'object' || Array.isArray(options)
332
+ || Object.values(options).some(value => !['boolean', 'string', 'number'].includes(typeof value)
333
+ || (typeof value === 'number' && !Number.isFinite(value)))) {
334
+ throw new HarmonyConfigError(`Invalid native module build options for ${name}.`);
335
+ }
336
+ }
337
+ }
338
+
381
339
  return Object.freeze({
340
+ ...(moduleBuilds && Object.keys(moduleBuilds).length ? { moduleBuilds: structuredClone(moduleBuilds) } : {}),
382
341
  abiFilters: abis,
383
342
  abilityName: ability,
384
343
  backgroundColor: background,
385
344
  bundleName: bundle,
386
- compatibleSdkVersionString: resolveSdkVersion(
387
- compatible,
388
- 'compatibleSdkVersion',
389
- harmony.compatibleSdkVersion
390
- ),
345
+ compatibleSdkVersionString: compatible.native,
391
346
  deviceTypes: devices,
392
347
  icon: harmony.icon || config.icon,
393
348
  label: readString(harmony.label, 'harmony.label', config.name),
@@ -398,12 +353,8 @@ function normalizeHarmonyConfig(config: HarmonyExpoConfig): NormalizedHarmonyCon
398
353
  querySchemes: queries,
399
354
  signingConfigFile: signing,
400
355
  skills,
401
- targetApiVersion: target,
402
- targetSdkVersionString: resolveSdkVersion(
403
- target,
404
- 'targetSdkVersion',
405
- harmony.targetSdkVersion
406
- ),
356
+ targetApiVersion: target.api,
357
+ targetSdkVersionString: target.native,
407
358
  vendor: readString(harmony.vendor, 'harmony.vendor', 'expo-harmony'),
408
359
  versionCode: code,
409
360
  versionName: version,
@@ -0,0 +1,69 @@
1
+ import { HarmonyConfigPluginError } from './errors';
2
+
3
+ export interface HarmonySdkVersion {
4
+ api: number | string;
5
+ native: string;
6
+ }
7
+
8
+ const Labels = new Map([
9
+ [13, '5.0.1(13)'], [14, '5.0.2(14)'], [20, '6.0.0(20)'],
10
+ [21, '6.0.1(21)'], [23, '6.1.0(23)'], [24, '6.1.1(24)'],
11
+ ]);
12
+
13
+ export function parseHarmonySdkVersion(value: unknown, field = 'SDK version'): HarmonySdkVersion {
14
+ if (typeof value === 'number') {
15
+ if (!Number.isSafeInteger(value) || value < 1 || value > 999) {
16
+ throw new HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `${field}: expected a positive API level.`);
17
+ }
18
+ if (value >= 26) return { api: `${value}.0.0`, native: `${value}.0.0` };
19
+
20
+ const native = Labels.get(value);
21
+
22
+ if (!native) {
23
+ throw new HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `${field}: API ${value} requires an explicit HarmonyOS SDK label.`);
24
+ }
25
+
26
+ return { api: value, native };
27
+ }
28
+ if (typeof value !== 'string') {
29
+ throw new HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `${field}: expected an API number or HarmonyOS SDK version.`);
30
+ }
31
+
32
+ const version = value.trim();
33
+ const dotted = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(version);
34
+
35
+ if (dotted && Number(dotted[1]) >= 26
36
+ && dotted.slice(1).every(part => String(Number(part)) === part)) {
37
+ return { api: version, native: version };
38
+ }
39
+
40
+ const label = /^\d+\.\d+\.\d+\(([1-9]\d*)\)$/.exec(version);
41
+
42
+ if (label && Number(label[1]) < 26) {
43
+ const api = Number(label[1]);
44
+ const known = Labels.get(api);
45
+
46
+ if (known && known !== version) {
47
+ throw new HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', `${field}: API ${api} uses SDK label ${known}.`);
48
+ }
49
+
50
+ return { api, native: version };
51
+ }
52
+
53
+ throw new HarmonyConfigPluginError(
54
+ 'ERR_HARMONY_CONFIG_INVALID',
55
+ `${field}: expected an SDK label such as "6.1.1(24)", or a dotted API version such as "26.0.0".`
56
+ );
57
+ }
58
+
59
+ export function compareHarmonyApiVersions(left: number | string, right: number | string): number {
60
+ const a = String(left).split('.').map(Number);
61
+ const b = String(right).split('.').map(Number);
62
+
63
+ for (let i = 0; i < 3; i++) {
64
+ const delta = (a[i] ?? 0) - (b[i] ?? 0);
65
+ if (delta) return delta;
66
+ }
67
+
68
+ return 0;
69
+ }