@expo-harmony/config-plugins 55.0.10-harmony.3 → 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
@@ -11,7 +11,7 @@ export type { HarmonyConfigPluginErrorOptions } from './errors';
11
11
  export { atomicWrite, stableJson as stableHarmonyJson } from './files';
12
12
  export { normalizeHarmonyConfig } from './normalizeConfig';
13
13
  export type { HarmonyExpoConfig, NormalizedHarmonyConfig } from './normalizeConfig';
14
- export { HarmonyModNames as HARMONY_MOD_NAMES, recordManagedFile, withAppJson, withArkTSPackageProvider, withCMakeLists, withColors, withCppPackageProvider, withEntryAbility, withEntryBuildProfile, withEntryHvigor, withEntryOhPackage, withHarmonyAutolinking, withHarmonyBaseMods, withHarmonyDangerousMod, withHarmonyMod, withHarmonyResources, withHvigorConfig, withIndexPage, withMedia, withModuleJson, withProfiles, withProjectBuildProfile, withReactNativeConfig, withRootHvigor, withRootOhPackage, withStrings, withWorker, } from './mods';
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
15
  export type { HarmonyJson, HarmonyMediaDescriptor, HarmonyMediaMap, HarmonyModName, HarmonyResourceMap, } from './mods';
16
16
  export { registerHarmonyConfigPlugin } from './ownership';
17
17
  export type { HarmonyConfigPluginOwnership } from './ownership';
@@ -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.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");
@@ -61,6 +61,7 @@ Object.defineProperty(exports, "withArkTSPackageProvider", { enumerable: true, g
61
61
  Object.defineProperty(exports, "withCMakeLists", { enumerable: true, get: function () { return mods_1.withCMakeLists; } });
62
62
  Object.defineProperty(exports, "withColors", { enumerable: true, get: function () { return mods_1.withColors; } });
63
63
  Object.defineProperty(exports, "withCppPackageProvider", { enumerable: true, get: function () { return mods_1.withCppPackageProvider; } });
64
+ Object.defineProperty(exports, "withAbilityStage", { enumerable: true, get: function () { return mods_1.withAbilityStage; } });
64
65
  Object.defineProperty(exports, "withEntryAbility", { enumerable: true, get: function () { return mods_1.withEntryAbility; } });
65
66
  Object.defineProperty(exports, "withEntryBuildProfile", { enumerable: true, get: function () { return mods_1.withEntryBuildProfile; } });
66
67
  Object.defineProperty(exports, "withEntryHvigor", { enumerable: true, get: function () { return mods_1.withEntryHvigor; } });
@@ -93,3 +94,6 @@ Object.defineProperty(exports, "withRawfile", { enumerable: true, get: function
93
94
  var permissions_1 = require("./permissions");
94
95
  Object.defineProperty(exports, "HarmonyPermissions", { enumerable: true, get: function () { return permissions_1.HarmonyPermissions; } });
95
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; } });
package/build/mods.d.ts CHANGED
@@ -3,7 +3,7 @@ import type { ExpoConfigWithHarmony } from './config';
3
3
  import type { HarmonyModuleJson } from './manifest';
4
4
  import type { HarmonyConfigPlugin, HarmonyModAction } from './pluginTypes';
5
5
  import type { HarmonyResourceFile } from './resources';
6
- export type HarmonyModName = 'dangerous' | 'reactNativeConfig' | 'appJson' | 'projectBuildProfile' | 'rootOhPackage' | 'rootHvigor' | 'hvigorConfig' | 'entryBuildProfile' | 'entryOhPackage' | 'entryHvigor' | 'moduleJson' | 'strings' | 'colors' | 'media' | 'profiles' | 'entryAbility' | 'indexPage' | 'worker' | 'arktsPackageProvider' | 'cppPackageProvider' | 'cmakeLists' | 'autolinking';
6
+ export type HarmonyModName = 'dangerous' | 'reactNativeConfig' | 'appJson' | 'projectBuildProfile' | 'rootOhPackage' | 'rootHvigor' | 'hvigorConfig' | 'entryBuildProfile' | 'entryOhPackage' | 'entryHvigor' | 'moduleJson' | 'strings' | 'colors' | 'media' | 'profiles' | 'abilityStage' | 'entryAbility' | 'indexPage' | 'worker' | 'arktsPackageProvider' | 'cppPackageProvider' | 'cmakeLists' | 'autolinking';
7
7
  export type HarmonyJson = Record<string, unknown>;
8
8
  export type HarmonyResourceMap = Record<string, HarmonyResourceFile>;
9
9
  export type HarmonyMediaDescriptor = {
@@ -42,6 +42,7 @@ export declare const withStrings: HarmonyConfigPlugin<HarmonyModAction<HarmonyRe
42
42
  export declare const withColors: HarmonyConfigPlugin<HarmonyModAction<HarmonyResourceMap>>;
43
43
  export declare const withMedia: HarmonyConfigPlugin<HarmonyModAction<HarmonyMediaMap>>;
44
44
  export declare const withProfiles: HarmonyConfigPlugin<HarmonyModAction<HarmonyJson>>;
45
+ export declare const withAbilityStage: HarmonyConfigPlugin<HarmonyModAction<string>>;
45
46
  export declare const withEntryAbility: HarmonyConfigPlugin<HarmonyModAction<string>>;
46
47
  export declare const withIndexPage: HarmonyConfigPlugin<HarmonyModAction<string>>;
47
48
  export declare const withWorker: HarmonyConfigPlugin<HarmonyModAction<string>>;
package/build/mods.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.withHarmonyResources = exports.withHarmonyDangerousMod = exports.withProjectPatch = exports.withPreparation = exports.withCngManifest = exports.withHarmonyAutolinking = exports.withCMakeLists = exports.withCppPackageProvider = exports.withArkTSPackageProvider = exports.withWorker = exports.withIndexPage = exports.withEntryAbility = exports.withProfiles = exports.withMedia = exports.withColors = exports.withStrings = exports.withModuleJson = exports.withEntryHvigor = exports.withEntryOhPackage = exports.withEntryBuildProfile = exports.withHvigorConfig = exports.withNativeInputsStamp = exports.withRootHvigor = exports.withRootOhPackage = exports.withProjectBuildProfile = exports.withAppJson = exports.withReactNativeConfig = exports.HarmonyModNames = void 0;
6
+ exports.withHarmonyResources = exports.withHarmonyDangerousMod = exports.withProjectPatch = exports.withPreparation = exports.withCngManifest = exports.withHarmonyAutolinking = exports.withCMakeLists = exports.withCppPackageProvider = exports.withArkTSPackageProvider = exports.withWorker = exports.withIndexPage = exports.withEntryAbility = exports.withAbilityStage = exports.withProfiles = exports.withMedia = exports.withColors = exports.withStrings = exports.withModuleJson = exports.withEntryHvigor = exports.withEntryOhPackage = exports.withEntryBuildProfile = exports.withHvigorConfig = exports.withNativeInputsStamp = exports.withRootHvigor = exports.withRootOhPackage = exports.withProjectBuildProfile = exports.withAppJson = exports.withReactNativeConfig = exports.HarmonyModNames = void 0;
7
7
  exports.recordManagedFile = recordManagedFile;
8
8
  exports.withHarmonyBaseMods = withHarmonyBaseMods;
9
9
  exports.withHarmonyMod = withHarmonyMod;
@@ -29,6 +29,7 @@ const TextMods = new Set([
29
29
  'rootHvigor',
30
30
  'nativeInputsStamp',
31
31
  'entryHvigor',
32
+ 'abilityStage',
32
33
  'entryAbility',
33
34
  'indexPage',
34
35
  'worker',
@@ -57,6 +58,7 @@ const AllModNames = Object.freeze([
57
58
  'colors',
58
59
  'media',
59
60
  'profiles',
61
+ 'abilityStage',
60
62
  'entryAbility',
61
63
  'indexPage',
62
64
  'worker',
@@ -397,6 +399,8 @@ const withMedia = (config, action) => withHarmonyMod(config, ['media', action]);
397
399
  exports.withMedia = withMedia;
398
400
  const withProfiles = (config, action) => withHarmonyMod(config, ['profiles', action]);
399
401
  exports.withProfiles = withProfiles;
402
+ const withAbilityStage = (config, action) => withHarmonyMod(config, ['abilityStage', action]);
403
+ exports.withAbilityStage = withAbilityStage;
400
404
  const withEntryAbility = (config, action) => withHarmonyMod(config, ['entryAbility', action]);
401
405
  exports.withEntryAbility = withEntryAbility;
402
406
  const withIndexPage = (config, action) => withHarmonyMod(config, ['indexPage', action]);
@@ -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})$/;
@@ -16,16 +17,8 @@ const Orientations = new Set([
16
17
  'auto_rotation',
17
18
  ]);
18
19
  const DeviceTypes = new Set(['phone', 'tablet', '2in1']);
19
- const MinimumApi = 13;
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
  }
@@ -219,6 +181,10 @@ function normalizeHarmonyConfig(config) {
219
181
  if (!permissions.some(permission => permission.name === 'ohos.permission.INTERNET')) {
220
182
  permissions.unshift({ name: 'ohos.permission.INTERNET' });
221
183
  }
184
+ if (config.updates?.checkAutomatically === 'WIFI_ONLY'
185
+ && !permissions.some(permission => permission.name === 'ohos.permission.GET_NETWORK_INFO')) {
186
+ permissions.push({ name: 'ohos.permission.GET_NETWORK_INFO' });
187
+ }
222
188
  const schemes = readExpoSchemes(config);
223
189
  let skills = (harmony.skills || []).map(normalizeSkill);
224
190
  for (const scheme of schemes) {
@@ -250,7 +216,7 @@ function normalizeHarmonyConfig(config) {
250
216
  abilityName: ability,
251
217
  backgroundColor: background,
252
218
  bundleName: bundle,
253
- compatibleSdkVersionString: resolveSdkVersion(compatible, 'compatibleSdkVersion', harmony.compatibleSdkVersion),
219
+ compatibleSdkVersionString: compatible.native,
254
220
  deviceTypes: devices,
255
221
  icon: harmony.icon || config.icon,
256
222
  label: readString(harmony.label, 'harmony.label', config.name),
@@ -261,8 +227,8 @@ function normalizeHarmonyConfig(config) {
261
227
  querySchemes: queries,
262
228
  signingConfigFile: signing,
263
229
  skills,
264
- targetApiVersion: target,
265
- targetSdkVersionString: resolveSdkVersion(target, 'targetSdkVersion', harmony.targetSdkVersion),
230
+ targetApiVersion: target.api,
231
+ targetSdkVersionString: target.native,
266
232
  vendor: readString(harmony.vendor, 'harmony.vendor', 'expo-harmony'),
267
233
  versionCode: code,
268
234
  versionName: version,
package/build/paths.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface HarmonyManagedPaths {
9
9
  readonly entryHvigor: string;
10
10
  readonly moduleJson: string;
11
11
  readonly profiles: string;
12
+ readonly abilityStage: string;
12
13
  readonly entryAbility: string;
13
14
  readonly indexPage: string;
14
15
  readonly worker: string;
package/build/paths.js CHANGED
@@ -24,6 +24,7 @@ const ManagedPaths = Object.freeze({
24
24
  entryHvigor: 'entry/hvigorfile.ts',
25
25
  moduleJson: 'entry/src/main/module.json5',
26
26
  profiles: 'entry/src/main/resources/base/profile/main_pages.json',
27
+ abilityStage: 'entry/src/main/ets/abilitystage/EntryAbilityStage.ets',
27
28
  entryAbility: 'entry/src/main/ets/entryability/EntryAbility.ets',
28
29
  indexPage: 'entry/src/main/ets/pages/Index.ets',
29
30
  worker: 'entry/src/main/ets/workers/RNOHWorker.ets',
@@ -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.3",
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
@@ -30,6 +30,7 @@ export {
30
30
  withCMakeLists,
31
31
  withColors,
32
32
  withCppPackageProvider,
33
+ withAbilityStage,
33
34
  withEntryAbility,
34
35
  withEntryBuildProfile,
35
36
  withEntryHvigor,
@@ -77,3 +78,6 @@ export type { HarmonyResourceFile, HarmonyResourceItem } from './resources';
77
78
  export { withHarmonyGeneratedFiles, withRawfile } from './generatedFiles';
78
79
  export type { HarmonyFileDescriptor, HarmonyFileMap, HarmonyGeneratedFilesOptions } from './generatedFiles';
79
80
  export { HarmonyPermissions, withHarmonyPermissions } from './permissions';
81
+
82
+ export { parseHarmonySdkVersion, compareHarmonyApiVersions } from './sdkVersion';
83
+ export type { HarmonySdkVersion } from './sdkVersion';
package/src/mods.ts CHANGED
@@ -34,6 +34,7 @@ export type HarmonyModName
34
34
  | 'colors'
35
35
  | 'media'
36
36
  | 'profiles'
37
+ | 'abilityStage'
37
38
  | 'entryAbility'
38
39
  | 'indexPage'
39
40
  | 'worker'
@@ -97,6 +98,7 @@ const TextMods = new Set<FileModName>([
97
98
  'rootHvigor',
98
99
  'nativeInputsStamp',
99
100
  'entryHvigor',
101
+ 'abilityStage',
100
102
  'entryAbility',
101
103
  'indexPage',
102
104
  'worker',
@@ -126,6 +128,7 @@ const AllModNames: readonly AnyModName[] = Object.freeze([
126
128
  'colors',
127
129
  'media',
128
130
  'profiles',
131
+ 'abilityStage',
129
132
  'entryAbility',
130
133
  'indexPage',
131
134
  'worker',
@@ -195,6 +198,7 @@ function withFileProvider(config: ExpoConfig, name: FileModName): ExpoConfig {
195
198
 
196
199
  const context = { ...request, modFile: file, modFileExists: exists } as typeof value.modRequest;
197
200
  const result = await next!({ ...value, modRequest: context, modResults: data });
201
+
198
202
  assertModResults(name, result.modResults, JsonMods.has(name) ? 'json' : 'text');
199
203
 
200
204
  if (!result.modRequest.introspect) {
@@ -239,6 +243,7 @@ async function readMediaMap(root: string): Promise<HarmonyMediaMap> {
239
243
  const directory = await resolveHarmonyPath(root, relative);
240
244
 
241
245
  media[scope] = {};
246
+
242
247
  let names: string[] = [];
243
248
 
244
249
  try {
@@ -292,6 +297,7 @@ async function writeMediaMap(
292
297
  { operation: 'harmony.media.write' }
293
298
  );
294
299
  }
300
+
295
301
  if (!descriptor || typeof descriptor !== 'object' || Array.isArray(descriptor)) {
296
302
  throw new HarmonyConfigPluginError(
297
303
  'ERR_HARMONY_CONFIG_INVALID',
@@ -308,6 +314,7 @@ async function writeMediaMap(
308
314
  { operation: 'harmony.media.write' }
309
315
  );
310
316
  }
317
+
311
318
  if (item.replaceBase !== undefined && typeof item.replaceBase !== 'boolean') {
312
319
  throw new HarmonyConfigPluginError(
313
320
  'ERR_HARMONY_CONFIG_INVALID',
@@ -414,6 +421,7 @@ function withResourceProvider(config: ExpoConfig, name: ResourceModName): ExpoCo
414
421
  : {};
415
422
 
416
423
  const result = await next!({ ...value, modRequest: request, modResults: data });
424
+
417
425
  assertModResults(name, result.modResults, 'resource');
418
426
 
419
427
  if (!result.modRequest.introspect) {
@@ -563,6 +571,7 @@ export function registerMod<T, Config extends ExpoConfigWithHarmony>(
563
571
  action: action as unknown as Parameters<typeof withMod<T>>[1]['action'],
564
572
  });
565
573
  const interceptor = (carrier.mods as unknown as Record<string, Record<string, HarmonyModAction>>).harmony[name];
574
+
566
575
  provider.expoHarmonyLateActions.push(interceptor);
567
576
 
568
577
  return config;
@@ -605,6 +614,8 @@ export const withMedia: HarmonyConfigPlugin<HarmonyModAction<HarmonyMediaMap>>
605
614
  = (config, action) => withHarmonyMod(config, ['media', action]);
606
615
  export const withProfiles: HarmonyConfigPlugin<HarmonyModAction<HarmonyJson>>
607
616
  = (config, action) => withHarmonyMod(config, ['profiles', action]);
617
+ export const withAbilityStage: HarmonyConfigPlugin<HarmonyModAction<string>>
618
+ = (config, action) => withHarmonyMod(config, ['abilityStage', action]);
608
619
  export const withEntryAbility: HarmonyConfigPlugin<HarmonyModAction<string>>
609
620
  = (config, action) => withHarmonyMod(config, ['entryAbility', action]);
610
621
  export const withIndexPage: HarmonyConfigPlugin<HarmonyModAction<string>>
@@ -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;
@@ -45,16 +46,8 @@ const Orientations = new Set([
45
46
  'auto_rotation',
46
47
  ]);
47
48
  const DeviceTypes = new Set<HarmonyDeviceType>(['phone', 'tablet', '2in1']);
48
- const MinimumApi = 13;
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
  }
@@ -338,6 +280,11 @@ function normalizeHarmonyConfig(config: HarmonyExpoConfig): NormalizedHarmonyCon
338
280
  permissions.unshift({ name: 'ohos.permission.INTERNET' });
339
281
  }
340
282
 
283
+ if (config.updates?.checkAutomatically === 'WIFI_ONLY'
284
+ && !permissions.some(permission => permission.name === 'ohos.permission.GET_NETWORK_INFO')) {
285
+ permissions.push({ name: 'ohos.permission.GET_NETWORK_INFO' });
286
+ }
287
+
341
288
  const schemes = readExpoSchemes(config);
342
289
  let skills = (harmony.skills || []).map(normalizeSkill);
343
290
 
@@ -378,11 +325,7 @@ function normalizeHarmonyConfig(config: HarmonyExpoConfig): NormalizedHarmonyCon
378
325
  abilityName: ability,
379
326
  backgroundColor: background,
380
327
  bundleName: bundle,
381
- compatibleSdkVersionString: resolveSdkVersion(
382
- compatible,
383
- 'compatibleSdkVersion',
384
- harmony.compatibleSdkVersion
385
- ),
328
+ compatibleSdkVersionString: compatible.native,
386
329
  deviceTypes: devices,
387
330
  icon: harmony.icon || config.icon,
388
331
  label: readString(harmony.label, 'harmony.label', config.name),
@@ -393,12 +336,8 @@ function normalizeHarmonyConfig(config: HarmonyExpoConfig): NormalizedHarmonyCon
393
336
  querySchemes: queries,
394
337
  signingConfigFile: signing,
395
338
  skills,
396
- targetApiVersion: target,
397
- targetSdkVersionString: resolveSdkVersion(
398
- target,
399
- 'targetSdkVersion',
400
- harmony.targetSdkVersion
401
- ),
339
+ targetApiVersion: target.api,
340
+ targetSdkVersionString: target.native,
402
341
  vendor: readString(harmony.vendor, 'harmony.vendor', 'expo-harmony'),
403
342
  versionCode: code,
404
343
  versionName: version,
package/src/paths.ts CHANGED
@@ -14,6 +14,7 @@ export interface HarmonyManagedPaths {
14
14
  readonly entryHvigor: string;
15
15
  readonly moduleJson: string;
16
16
  readonly profiles: string;
17
+ readonly abilityStage: string;
17
18
  readonly entryAbility: string;
18
19
  readonly indexPage: string;
19
20
  readonly worker: string;
@@ -60,6 +61,7 @@ const ManagedPaths: HarmonyManagedPaths & { readonly nativeInputsStamp: string }
60
61
  entryHvigor: 'entry/hvigorfile.ts',
61
62
  moduleJson: 'entry/src/main/module.json5',
62
63
  profiles: 'entry/src/main/resources/base/profile/main_pages.json',
64
+ abilityStage: 'entry/src/main/ets/abilitystage/EntryAbilityStage.ets',
63
65
  entryAbility: 'entry/src/main/ets/entryability/EntryAbility.ets',
64
66
  indexPage: 'entry/src/main/ets/pages/Index.ets',
65
67
  worker: 'entry/src/main/ets/workers/RNOHWorker.ets',
@@ -114,6 +116,7 @@ export async function assertNoExternalSymlink(root: string, target: string): Pro
114
116
  current = path.join(current, segment);
115
117
 
116
118
  let stat: fs.Stats;
119
+
117
120
  try {
118
121
  stat = await fs.promises.lstat(current);
119
122
  } catch (cause) {
@@ -173,6 +176,7 @@ export async function resolveProjectPath(
173
176
  name: keyof HarmonyProjectPaths
174
177
  ): Promise<string> {
175
178
  const candidates = ProjectPathCandidates[name] || [ProjectPaths[name]];
179
+
176
180
  for (const candidate of candidates) {
177
181
  if (fs.existsSync(path.join(root, candidate))) return resolveHarmonyPath(root, candidate);
178
182
  }
@@ -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
+ }