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

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,7 @@ export interface HarmonyConfig {
26
26
  vendor?: string;
27
27
  versionCode?: number;
28
28
  versionName?: string;
29
+ /** @deprecated Configure targetSdkVersion in @expo-harmony/expo-build-properties. */
29
30
  targetApiVersion?: number;
30
31
  targetSdkVersion?: number | string;
31
32
  compatibleSdkVersion?: number | string;
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; } });
@@ -16,7 +16,7 @@ interface NormalizedHarmonyConfig {
16
16
  querySchemes: string[];
17
17
  signingConfigFile?: string;
18
18
  skills: NonNullable<HarmonyConfig['skills']>;
19
- targetApiVersion: number;
19
+ targetApiVersion: number | string;
20
20
  targetSdkVersionString: string;
21
21
  vendor: string;
22
22
  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
  }
@@ -254,7 +216,7 @@ function normalizeHarmonyConfig(config) {
254
216
  abilityName: ability,
255
217
  backgroundColor: background,
256
218
  bundleName: bundle,
257
- compatibleSdkVersionString: resolveSdkVersion(compatible, 'compatibleSdkVersion', harmony.compatibleSdkVersion),
219
+ compatibleSdkVersionString: compatible.native,
258
220
  deviceTypes: devices,
259
221
  icon: harmony.icon || config.icon,
260
222
  label: readString(harmony.label, 'harmony.label', config.name),
@@ -265,8 +227,8 @@ function normalizeHarmonyConfig(config) {
265
227
  querySchemes: queries,
266
228
  signingConfigFile: signing,
267
229
  skills,
268
- targetApiVersion: target,
269
- targetSdkVersionString: resolveSdkVersion(target, 'targetSdkVersion', harmony.targetSdkVersion),
230
+ targetApiVersion: target.api,
231
+ targetSdkVersionString: target.native,
270
232
  vendor: readString(harmony.vendor, 'harmony.vendor', 'expo-harmony'),
271
233
  versionCode: code,
272
234
  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.10-harmony.5",
4
4
  "keywords": [
5
5
  "react-native",
6
6
  "expo",
@@ -46,9 +46,6 @@
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,7 @@ export interface HarmonyConfig {
34
34
  vendor?: string;
35
35
  versionCode?: number;
36
36
  versionName?: string;
37
+ /** @deprecated Configure targetSdkVersion in @expo-harmony/expo-build-properties. */
37
38
  targetApiVersion?: number;
38
39
  targetSdkVersion?: number | string;
39
40
  compatibleSdkVersion?: number | string;
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
 
@@ -26,7 +27,7 @@ interface NormalizedHarmonyConfig {
26
27
  querySchemes: string[];
27
28
  signingConfigFile?: string;
28
29
  skills: NonNullable<HarmonyConfig['skills']>;
29
- targetApiVersion: number;
30
+ targetApiVersion: number | string;
30
31
  targetSdkVersionString: string;
31
32
  vendor: string;
32
33
  versionCode: number;
@@ -47,14 +48,6 @@ const Orientations = new Set([
47
48
  const DeviceTypes = new Set<HarmonyDeviceType>(['phone', 'tablet', '2in1']);
48
49
  const MinimumApi = 20;
49
50
  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
51
 
59
52
  class HarmonyConfigError extends HarmonyConfigPluginError {
60
53
  constructor(message: string) {
@@ -96,58 +89,6 @@ function normalizeColor(value: unknown, field: string, fallback: string): string
96
89
  return toArgb(color);
97
90
  }
98
91
 
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
92
  function normalizeStringArray<T extends string = string>(
152
93
  value: unknown,
153
94
  field: string,
@@ -286,24 +227,25 @@ function normalizeHarmonyConfig(config: HarmonyExpoConfig): NormalizedHarmonyCon
286
227
  const version = readString(harmony.versionName, 'harmony.versionName', config.version || '1.0.0');
287
228
  const code = readPositiveInteger(harmony.versionCode, 'versionCode', 1);
288
229
 
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
230
+ const target = parseHarmonySdkVersion(
231
+ harmony.targetSdkVersion ?? harmony.targetApiVersion ?? TargetApi, 'harmony.targetSdkVersion'
298
232
  );
299
233
 
300
- if (compatible < MinimumApi) {
301
- throw new HarmonyConfigError(
302
- `Harmony compatible API must be ${MinimumApi} or newer.`
303
- );
234
+ if (harmony.targetApiVersion !== undefined) {
235
+ const alias = parseHarmonySdkVersion(harmony.targetApiVersion, 'harmony.targetApiVersion');
236
+
237
+ if (compareHarmonyApiVersions(alias.api, target.api) !== 0) {
238
+ throw new HarmonyConfigError('harmony.targetApiVersion conflicts with harmony.targetSdkVersion.');
239
+ }
240
+ }
241
+
242
+ const compatible = parseHarmonySdkVersion(harmony.compatibleSdkVersion ?? MinimumApi, 'harmony.compatibleSdkVersion');
243
+
244
+ if (compareHarmonyApiVersions(compatible.api, MinimumApi) < 0) {
245
+ throw new HarmonyConfigError(`Harmony compatible API must be ${MinimumApi} or newer.`);
304
246
  }
305
- if (compatible > target) {
306
- throw new HarmonyConfigError('Harmony compatibleSdkVersion cannot exceed targetApiVersion.');
247
+ if (compareHarmonyApiVersions(compatible.api, target.api) > 0) {
248
+ throw new HarmonyConfigError('Harmony compatibleSdkVersion cannot exceed targetSdkVersion.');
307
249
  }
308
250
 
309
251
  const orientation = harmony.orientation || config.orientation || 'default';
@@ -327,7 +269,7 @@ function normalizeHarmonyConfig(config: HarmonyExpoConfig): NormalizedHarmonyCon
327
269
  : normalizeStringArray(harmony.abiFilters, 'harmony.abiFilters');
328
270
 
329
271
  for (const abi of abis) {
330
- if (!/^[A-Za-z0-9_-]+$/.test(abi)) {
272
+ if (!['arm64-v8a', 'x86_64'].includes(abi)) {
331
273
  throw new HarmonyConfigError(`Invalid Harmony ABI filter: ${abi}`);
332
274
  }
333
275
  }
@@ -383,11 +325,7 @@ function normalizeHarmonyConfig(config: HarmonyExpoConfig): NormalizedHarmonyCon
383
325
  abilityName: ability,
384
326
  backgroundColor: background,
385
327
  bundleName: bundle,
386
- compatibleSdkVersionString: resolveSdkVersion(
387
- compatible,
388
- 'compatibleSdkVersion',
389
- harmony.compatibleSdkVersion
390
- ),
328
+ compatibleSdkVersionString: compatible.native,
391
329
  deviceTypes: devices,
392
330
  icon: harmony.icon || config.icon,
393
331
  label: readString(harmony.label, 'harmony.label', config.name),
@@ -398,12 +336,8 @@ function normalizeHarmonyConfig(config: HarmonyExpoConfig): NormalizedHarmonyCon
398
336
  querySchemes: queries,
399
337
  signingConfigFile: signing,
400
338
  skills,
401
- targetApiVersion: target,
402
- targetSdkVersionString: resolveSdkVersion(
403
- target,
404
- 'targetSdkVersion',
405
- harmony.targetSdkVersion
406
- ),
339
+ targetApiVersion: target.api,
340
+ targetSdkVersionString: target.native,
407
341
  vendor: readString(harmony.vendor, 'harmony.vendor', 'expo-harmony'),
408
342
  versionCode: code,
409
343
  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
+ }