@expo-harmony/config-plugins 55.0.10-harmony.2 → 55.0.10-harmony.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +203 -11
- package/build/compiler.d.ts +7 -0
- package/build/compiler.js +12 -0
- package/build/composition.d.ts +24 -0
- package/build/composition.js +11 -0
- package/build/config.d.ts +2 -15
- package/build/customFiles.d.ts +15 -0
- package/build/customFiles.js +108 -0
- package/build/files.d.ts +1 -1
- package/build/files.js +2 -2
- package/build/generatedFiles.d.ts +15 -0
- package/build/generatedFiles.js +70 -0
- package/build/index.d.ts +20 -5
- package/build/index.js +53 -6
- package/build/internal.d.ts +3 -0
- package/build/internal.js +13 -0
- package/build/manifest.d.ts +38 -0
- package/build/manifest.js +71 -0
- package/build/mods.d.ts +37 -31
- package/build/mods.js +118 -86
- package/build/normalizeConfig.d.ts +10 -3
- package/build/normalizeConfig.js +45 -47
- package/build/ownership.d.ts +4 -3
- package/build/ownership.js +17 -0
- package/build/paths.d.ts +13 -10
- package/build/paths.js +32 -31
- package/build/permissions.d.ts +12 -0
- package/build/permissions.js +60 -0
- package/build/pluginTypes.d.ts +15 -0
- package/build/pluginTypes.js +2 -0
- package/build/resources.d.ts +13 -0
- package/build/resources.js +55 -0
- package/package.json +9 -9
- package/src/compiler.ts +21 -0
- package/src/composition.ts +45 -0
- package/src/config.ts +3 -19
- package/src/customFiles.ts +142 -0
- package/src/files.ts +3 -4
- package/src/generatedFiles.ts +85 -0
- package/src/index.ts +20 -11
- package/src/internal.ts +3 -0
- package/src/manifest.ts +151 -0
- package/src/mods.ts +180 -128
- package/src/nativeInputs.ts +0 -1
- package/src/normalizeConfig.ts +56 -59
- package/src/ownership.ts +25 -5
- package/src/paths.ts +41 -45
- package/src/permissions.ts +68 -0
- package/src/pluginTypes.ts +24 -0
- package/src/resources.ts +65 -0
package/src/normalizeConfig.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
HarmonySkill,
|
|
7
7
|
} from './config';
|
|
8
8
|
import { HarmonyConfigPluginError } from './errors';
|
|
9
|
+
import { toArgb } from './resources';
|
|
9
10
|
|
|
10
11
|
type HarmonyExpoConfig = ExpoConfigWithHarmony;
|
|
11
12
|
|
|
@@ -32,10 +33,10 @@ interface NormalizedHarmonyConfig {
|
|
|
32
33
|
versionName: string;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
const
|
|
36
|
+
const BundlePattern = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*){2,}$/;
|
|
36
37
|
const IdentifierPattern = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
37
38
|
const ColorPattern = /^(#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{8})$/;
|
|
38
|
-
const
|
|
39
|
+
const Orientations = new Set([
|
|
39
40
|
'default',
|
|
40
41
|
'portrait',
|
|
41
42
|
'landscape',
|
|
@@ -43,13 +44,14 @@ const ValidOrientations = new Set([
|
|
|
43
44
|
'landscape_inverted',
|
|
44
45
|
'auto_rotation',
|
|
45
46
|
]);
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const HarmonySdkVersions = new Map([
|
|
47
|
+
const DeviceTypes = new Set<HarmonyDeviceType>(['phone', 'tablet', '2in1']);
|
|
48
|
+
const MinimumApi = 20;
|
|
49
|
+
const TargetApi = 24;
|
|
50
|
+
const SdkVersions = new Map([
|
|
51
51
|
[13, '5.0.1(13)'],
|
|
52
|
+
[14, '5.0.2(14)'],
|
|
52
53
|
[20, '6.0.0(20)'],
|
|
54
|
+
[21, '6.0.1(21)'],
|
|
53
55
|
[23, '6.1.0(23)'],
|
|
54
56
|
[24, '6.1.1(24)'],
|
|
55
57
|
]);
|
|
@@ -91,11 +93,7 @@ function normalizeColor(value: unknown, field: string, fallback: string): string
|
|
|
91
93
|
throw new HarmonyConfigError(`${field} must be #RRGGBB or #RRGGBBAA.`);
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return result.length === 9
|
|
97
|
-
? `#${result.slice(7, 9)}${result.slice(1, 7)}`
|
|
98
|
-
: result;
|
|
96
|
+
return toArgb(color);
|
|
99
97
|
}
|
|
100
98
|
|
|
101
99
|
function parseSdkApi(value: unknown, field: string): number | null {
|
|
@@ -127,11 +125,11 @@ function resolveSdkVersion(api: number, field: string, value?: unknown): string
|
|
|
127
125
|
}
|
|
128
126
|
} else {
|
|
129
127
|
const version = readString(value, `harmony.${field}`);
|
|
130
|
-
const
|
|
128
|
+
const parsed = parseSdkApi(version, field);
|
|
131
129
|
|
|
132
|
-
if (
|
|
130
|
+
if (parsed !== api) {
|
|
133
131
|
throw new HarmonyConfigError(
|
|
134
|
-
`harmony.${field} describes API ${
|
|
132
|
+
`harmony.${field} describes API ${parsed}, but the configured API level is ${api}.`
|
|
135
133
|
);
|
|
136
134
|
}
|
|
137
135
|
|
|
@@ -139,7 +137,7 @@ function resolveSdkVersion(api: number, field: string, value?: unknown): string
|
|
|
139
137
|
}
|
|
140
138
|
}
|
|
141
139
|
|
|
142
|
-
const version =
|
|
140
|
+
const version = SdkVersions.get(api);
|
|
143
141
|
|
|
144
142
|
if (!version) {
|
|
145
143
|
throw new HarmonyConfigError(
|
|
@@ -171,7 +169,7 @@ function normalizeStringArray<T extends string = string>(
|
|
|
171
169
|
return result;
|
|
172
170
|
}
|
|
173
171
|
|
|
174
|
-
function normalizePermission(permission: HarmonyPermission) {
|
|
172
|
+
export function normalizePermission(permission: HarmonyPermission) {
|
|
175
173
|
if (!permission || typeof permission !== 'object') {
|
|
176
174
|
throw new HarmonyConfigError('harmony.permissions entries must be objects.');
|
|
177
175
|
}
|
|
@@ -196,6 +194,7 @@ function normalizePermission(permission: HarmonyPermission) {
|
|
|
196
194
|
if (!permission.usedScene || typeof permission.usedScene !== 'object') {
|
|
197
195
|
throw new HarmonyConfigError('permission.usedScene must be an object.');
|
|
198
196
|
}
|
|
197
|
+
|
|
199
198
|
const when: 'always' | 'inuse' = permission.usedScene.when === undefined
|
|
200
199
|
? 'inuse'
|
|
201
200
|
: permission.usedScene.when;
|
|
@@ -221,7 +220,7 @@ function normalizeSkill(skill: HarmonySkill) {
|
|
|
221
220
|
const result: {
|
|
222
221
|
actions?: string[];
|
|
223
222
|
entities?: string[];
|
|
224
|
-
uris?:
|
|
223
|
+
uris?: HarmonySkill['uris'];
|
|
225
224
|
} = {};
|
|
226
225
|
|
|
227
226
|
for (const field of ['entities', 'actions'] as const) {
|
|
@@ -238,9 +237,11 @@ function normalizeSkill(skill: HarmonySkill) {
|
|
|
238
237
|
throw new HarmonyConfigError('Harmony skill URI must be an object.');
|
|
239
238
|
}
|
|
240
239
|
|
|
241
|
-
const result:
|
|
240
|
+
const result: NonNullable<HarmonySkill['uris']>[number] = {};
|
|
242
241
|
for (const [key, value] of Object.entries(uri)) {
|
|
243
|
-
result[key] =
|
|
242
|
+
result[key] = typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value))
|
|
243
|
+
? value
|
|
244
|
+
: readString(value, `harmony.skills[].uris[].${key}`);
|
|
244
245
|
}
|
|
245
246
|
|
|
246
247
|
return result;
|
|
@@ -266,67 +267,57 @@ function normalizeHarmonyConfig(config: HarmonyExpoConfig): NormalizedHarmonyCon
|
|
|
266
267
|
throw new HarmonyConfigError('Expo config must contain a harmony object.');
|
|
267
268
|
}
|
|
268
269
|
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
if (!BundleNamePattern.test(bundleName)) {
|
|
270
|
+
const bundle = readString(harmony.bundleName, 'harmony.bundleName');
|
|
271
|
+
if (!BundlePattern.test(bundle)) {
|
|
272
272
|
throw new HarmonyConfigError(
|
|
273
273
|
'harmony.bundleName must contain at least three valid dot-separated segments.'
|
|
274
274
|
);
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
if (!IdentifierPattern.test(moduleName)) {
|
|
277
|
+
const module = readString(harmony.moduleName, 'harmony.moduleName', 'entry');
|
|
278
|
+
const ability = readString(harmony.abilityName, 'harmony.abilityName', 'EntryAbility');
|
|
279
|
+
if (!IdentifierPattern.test(module)) {
|
|
281
280
|
throw new HarmonyConfigError('harmony.moduleName must be a valid Harmony identifier.');
|
|
282
281
|
}
|
|
283
|
-
if (!IdentifierPattern.test(
|
|
282
|
+
if (!IdentifierPattern.test(ability)) {
|
|
284
283
|
throw new HarmonyConfigError('harmony.abilityName must be a valid Harmony identifier.');
|
|
285
284
|
}
|
|
286
285
|
|
|
287
|
-
const
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
const
|
|
286
|
+
const version = readString(harmony.versionName, 'harmony.versionName', config.version || '1.0.0');
|
|
287
|
+
const code = readPositiveInteger(harmony.versionCode, 'versionCode', 1);
|
|
288
|
+
|
|
289
|
+
const target = readPositiveInteger(
|
|
291
290
|
harmony.targetApiVersion,
|
|
292
291
|
'targetApiVersion',
|
|
293
|
-
|
|
292
|
+
parseSdkApi(harmony.targetSdkVersion, 'targetSdkVersion') ?? TargetApi
|
|
294
293
|
);
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
compatibleSdkApi ?? undefined,
|
|
294
|
+
const compatible = readPositiveInteger(
|
|
295
|
+
parseSdkApi(harmony.compatibleSdkVersion, 'compatibleSdkVersion') ?? undefined,
|
|
298
296
|
'compatibleSdkVersion',
|
|
299
|
-
|
|
297
|
+
MinimumApi
|
|
300
298
|
);
|
|
301
299
|
|
|
302
|
-
if (
|
|
300
|
+
if (compatible < MinimumApi) {
|
|
303
301
|
throw new HarmonyConfigError(
|
|
304
|
-
`Harmony compatible API must be ${
|
|
302
|
+
`Harmony compatible API must be ${MinimumApi} or newer.`
|
|
305
303
|
);
|
|
306
304
|
}
|
|
307
|
-
if (
|
|
305
|
+
if (compatible > target) {
|
|
308
306
|
throw new HarmonyConfigError('Harmony compatibleSdkVersion cannot exceed targetApiVersion.');
|
|
309
307
|
}
|
|
310
308
|
|
|
311
309
|
const orientation = harmony.orientation || config.orientation || 'default';
|
|
312
|
-
|
|
313
|
-
if (!ValidOrientations.has(orientation)) {
|
|
310
|
+
if (!Orientations.has(orientation)) {
|
|
314
311
|
throw new HarmonyConfigError(`Unsupported Harmony orientation: ${orientation}`);
|
|
315
312
|
}
|
|
316
313
|
|
|
317
|
-
const style = harmony.userInterfaceStyle || config.userInterfaceStyle || 'light';
|
|
318
|
-
|
|
319
|
-
if (!['light', 'dark', 'automatic'].includes(style)) {
|
|
320
|
-
throw new HarmonyConfigError(`Unsupported Harmony UI style: ${style}`);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
314
|
const background = normalizeColor(
|
|
324
315
|
harmony.backgroundColor || config.backgroundColor,
|
|
325
316
|
'harmony.backgroundColor',
|
|
326
317
|
'#FFFFFF'
|
|
327
318
|
);
|
|
328
|
-
const engine = harmony.jsEngine || 'hermes';
|
|
329
319
|
|
|
320
|
+
const engine = harmony.jsEngine || 'hermes';
|
|
330
321
|
if (engine !== 'hermes') {
|
|
331
322
|
throw new HarmonyConfigError('Expo Harmony currently supports only the Hermes JavaScript engine.');
|
|
332
323
|
}
|
|
@@ -347,6 +338,11 @@ function normalizeHarmonyConfig(config: HarmonyExpoConfig): NormalizedHarmonyCon
|
|
|
347
338
|
permissions.unshift({ name: 'ohos.permission.INTERNET' });
|
|
348
339
|
}
|
|
349
340
|
|
|
341
|
+
if (config.updates?.checkAutomatically === 'WIFI_ONLY'
|
|
342
|
+
&& !permissions.some(permission => permission.name === 'ohos.permission.GET_NETWORK_INFO')) {
|
|
343
|
+
permissions.push({ name: 'ohos.permission.GET_NETWORK_INFO' });
|
|
344
|
+
}
|
|
345
|
+
|
|
350
346
|
const schemes = readExpoSchemes(config);
|
|
351
347
|
let skills = (harmony.skills || []).map(normalizeSkill);
|
|
352
348
|
|
|
@@ -359,6 +355,7 @@ function normalizeHarmonyConfig(config: HarmonyExpoConfig): NormalizedHarmonyCon
|
|
|
359
355
|
}
|
|
360
356
|
|
|
361
357
|
skills = [...new Map(skills.map(skill => [JSON.stringify(skill), skill])).values()];
|
|
358
|
+
|
|
362
359
|
const queries = [...new Set([
|
|
363
360
|
'http',
|
|
364
361
|
'https',
|
|
@@ -378,40 +375,40 @@ function normalizeHarmonyConfig(config: HarmonyExpoConfig): NormalizedHarmonyCon
|
|
|
378
375
|
: normalizeStringArray<HarmonyDeviceType>(
|
|
379
376
|
harmony.deviceTypes,
|
|
380
377
|
'harmony.deviceTypes',
|
|
381
|
-
|
|
378
|
+
DeviceTypes
|
|
382
379
|
);
|
|
383
380
|
|
|
384
381
|
return Object.freeze({
|
|
385
382
|
abiFilters: abis,
|
|
386
|
-
abilityName,
|
|
383
|
+
abilityName: ability,
|
|
387
384
|
backgroundColor: background,
|
|
388
|
-
bundleName,
|
|
385
|
+
bundleName: bundle,
|
|
389
386
|
compatibleSdkVersionString: resolveSdkVersion(
|
|
390
|
-
|
|
387
|
+
compatible,
|
|
391
388
|
'compatibleSdkVersion',
|
|
392
389
|
harmony.compatibleSdkVersion
|
|
393
390
|
),
|
|
394
391
|
deviceTypes: devices,
|
|
395
392
|
icon: harmony.icon || config.icon,
|
|
396
393
|
label: readString(harmony.label, 'harmony.label', config.name),
|
|
397
|
-
moduleName,
|
|
394
|
+
moduleName: module,
|
|
398
395
|
nativeOrientation: orientation === 'default' ? 'unspecified' : orientation,
|
|
399
396
|
permissions,
|
|
400
397
|
productName: readString(harmony.productName, 'harmony.productName', 'default'),
|
|
401
398
|
querySchemes: queries,
|
|
402
399
|
signingConfigFile: signing,
|
|
403
400
|
skills,
|
|
404
|
-
targetApiVersion:
|
|
401
|
+
targetApiVersion: target,
|
|
405
402
|
targetSdkVersionString: resolveSdkVersion(
|
|
406
|
-
|
|
403
|
+
target,
|
|
407
404
|
'targetSdkVersion',
|
|
408
405
|
harmony.targetSdkVersion
|
|
409
406
|
),
|
|
410
407
|
vendor: readString(harmony.vendor, 'harmony.vendor', 'expo-harmony'),
|
|
411
|
-
versionCode,
|
|
412
|
-
versionName,
|
|
408
|
+
versionCode: code,
|
|
409
|
+
versionName: version,
|
|
413
410
|
});
|
|
414
411
|
}
|
|
415
412
|
|
|
416
|
-
export {
|
|
413
|
+
export { normalizeHarmonyConfig };
|
|
417
414
|
export type { HarmonyExpoConfig, NormalizedHarmonyConfig };
|
package/src/ownership.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ExpoConfigWithHarmony } from './config';
|
|
2
2
|
|
|
3
3
|
export interface HarmonyConfigPluginOwnership {
|
|
4
4
|
readonly owner: string;
|
|
5
|
+
readonly files?: readonly string[];
|
|
5
6
|
readonly ability?: Readonly<Partial<{
|
|
6
7
|
startWindowBackground: string;
|
|
7
8
|
startWindowIcon: string;
|
|
@@ -115,7 +116,19 @@ export function normalizeHarmonyConfigPlugins(value: unknown): readonly HarmonyC
|
|
|
115
116
|
|
|
116
117
|
owners.add(plugin.owner);
|
|
117
118
|
|
|
119
|
+
let files: string[] | undefined;
|
|
120
|
+
if (plugin.files !== undefined) {
|
|
121
|
+
if (!Array.isArray(plugin.files) || plugin.files.some(file => typeof file !== 'string'
|
|
122
|
+
|| !file || file.includes('\\') || file.includes('\0') || /^[A-Za-z]:/.test(file)
|
|
123
|
+
|| file.split('/').some(part => !part || part === '.' || part === '..'))) {
|
|
124
|
+
throw new TypeError(`Invalid Harmony config-plugin ownership: ${plugin.owner}.files must contain relative file paths.`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
files = [...new Set(plugin.files as string[])].sort();
|
|
128
|
+
}
|
|
129
|
+
|
|
118
130
|
return {
|
|
131
|
+
...(files === undefined ? {} : { files }),
|
|
119
132
|
ability: normalizeAbility(plugin.ability),
|
|
120
133
|
owner: plugin.owner,
|
|
121
134
|
resources: normalizeResources(plugin.resources),
|
|
@@ -125,15 +138,22 @@ export function normalizeHarmonyConfigPlugins(value: unknown): readonly HarmonyC
|
|
|
125
138
|
return plugins.sort((left, right) => left.owner.localeCompare(right.owner, 'en'));
|
|
126
139
|
}
|
|
127
140
|
|
|
128
|
-
export function registerHarmonyConfigPlugin(
|
|
129
|
-
config:
|
|
141
|
+
export function registerHarmonyConfigPlugin<Config extends ExpoConfigWithHarmony>(
|
|
142
|
+
config: Config,
|
|
130
143
|
owner: string,
|
|
131
144
|
claims: Omit<HarmonyConfigPluginOwnership, 'owner'> = {}
|
|
132
|
-
):
|
|
145
|
+
): Config {
|
|
133
146
|
const plugin = normalizeHarmonyConfigPlugins([{ ...claims, owner }])[0];
|
|
134
147
|
|
|
135
148
|
config._internal ??= {};
|
|
136
149
|
const plugins = normalizeHarmonyConfigPlugins(config._internal.harmonyConfigPlugins);
|
|
150
|
+
for (const other of plugins) {
|
|
151
|
+
if (other.owner === owner) continue;
|
|
152
|
+
|
|
153
|
+
const duplicate = plugin.files?.find(file => other.files?.includes(file));
|
|
154
|
+
if (duplicate) throw new TypeError(`Harmony file ${duplicate} is claimed by both ${other.owner} and ${owner}.`);
|
|
155
|
+
}
|
|
156
|
+
|
|
137
157
|
config._internal.harmonyConfigPlugins = normalizeHarmonyConfigPlugins([
|
|
138
158
|
...plugins.filter(item => item.owner !== owner),
|
|
139
159
|
plugin,
|
|
@@ -142,6 +162,6 @@ export function registerHarmonyConfigPlugin(
|
|
|
142
162
|
return config;
|
|
143
163
|
}
|
|
144
164
|
|
|
145
|
-
export function getHarmonyConfigPlugins(config:
|
|
165
|
+
export function getHarmonyConfigPlugins(config: ExpoConfigWithHarmony): readonly HarmonyConfigPluginOwnership[] {
|
|
146
166
|
return normalizeHarmonyConfigPlugins(config?._internal?.harmonyConfigPlugins);
|
|
147
167
|
}
|
package/src/paths.ts
CHANGED
|
@@ -8,13 +8,13 @@ export interface HarmonyManagedPaths {
|
|
|
8
8
|
readonly projectBuildProfile: string;
|
|
9
9
|
readonly rootOhPackage: string;
|
|
10
10
|
readonly rootHvigor: string;
|
|
11
|
-
readonly nativeInputsStamp: string;
|
|
12
11
|
readonly hvigorConfig: string;
|
|
13
12
|
readonly entryBuildProfile: string;
|
|
14
13
|
readonly entryOhPackage: string;
|
|
15
14
|
readonly entryHvigor: string;
|
|
16
15
|
readonly moduleJson: string;
|
|
17
16
|
readonly profiles: string;
|
|
17
|
+
readonly abilityStage: string;
|
|
18
18
|
readonly entryAbility: string;
|
|
19
19
|
readonly indexPage: string;
|
|
20
20
|
readonly worker: string;
|
|
@@ -44,12 +44,12 @@ export interface HarmonyPathsApi {
|
|
|
44
44
|
readonly RESOURCE_PATHS: HarmonyResourcePaths;
|
|
45
45
|
assertNoExternalSymlink(root: string, target: string): Promise<void>;
|
|
46
46
|
isInside(root: string, target: string): boolean;
|
|
47
|
-
resolveHarmonyPath(
|
|
48
|
-
resolveProjectPath(
|
|
49
|
-
toPosixRelative(
|
|
47
|
+
resolveHarmonyPath(directory: string, relative: string): Promise<string>;
|
|
48
|
+
resolveProjectPath(root: string, name: keyof HarmonyProjectPaths): Promise<string>;
|
|
49
|
+
toPosixRelative(root: string, target: string): string;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
const ManagedPaths: HarmonyManagedPaths = Object.freeze({
|
|
52
|
+
const ManagedPaths: HarmonyManagedPaths & { readonly nativeInputsStamp: string } = Object.freeze({
|
|
53
53
|
appJson: 'AppScope/app.json5',
|
|
54
54
|
projectBuildProfile: 'build-profile.json5',
|
|
55
55
|
rootOhPackage: 'oh-package.json5',
|
|
@@ -61,6 +61,7 @@ const ManagedPaths: HarmonyManagedPaths = Object.freeze({
|
|
|
61
61
|
entryHvigor: 'entry/hvigorfile.ts',
|
|
62
62
|
moduleJson: 'entry/src/main/module.json5',
|
|
63
63
|
profiles: 'entry/src/main/resources/base/profile/main_pages.json',
|
|
64
|
+
abilityStage: 'entry/src/main/ets/abilitystage/EntryAbilityStage.ets',
|
|
64
65
|
entryAbility: 'entry/src/main/ets/entryability/EntryAbility.ets',
|
|
65
66
|
indexPage: 'entry/src/main/ets/pages/Index.ets',
|
|
66
67
|
worker: 'entry/src/main/ets/workers/RNOHWorker.ets',
|
|
@@ -74,8 +75,7 @@ const ProjectPaths: HarmonyProjectPaths = Object.freeze({
|
|
|
74
75
|
});
|
|
75
76
|
|
|
76
77
|
const ProjectPathCandidates: HarmonyProjectPathCandidates = Object.freeze({
|
|
77
|
-
//
|
|
78
|
-
// search places. The first entry is also the file created for new projects.
|
|
78
|
+
// Match React Native CLI lookup precedence; new projects use the first path.
|
|
79
79
|
reactNativeConfig: Object.freeze([
|
|
80
80
|
'react-native.config.js',
|
|
81
81
|
'react-native.config.cjs',
|
|
@@ -102,41 +102,41 @@ const ResourcePaths: HarmonyResourcePaths = Object.freeze({
|
|
|
102
102
|
|
|
103
103
|
export function isInside(root: string, target: string): boolean {
|
|
104
104
|
const relative = path.relative(root, target);
|
|
105
|
-
|
|
106
105
|
return relative === ''
|
|
107
106
|
|| (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
108
107
|
}
|
|
109
108
|
|
|
110
109
|
export async function assertNoExternalSymlink(root: string, target: string): Promise<void> {
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const relative = path.relative(
|
|
114
|
-
let
|
|
110
|
+
const absolute = path.resolve(root);
|
|
111
|
+
const canonical = await fs.promises.realpath(absolute).catch(() => absolute);
|
|
112
|
+
const relative = path.relative(absolute, target);
|
|
113
|
+
let current = absolute;
|
|
115
114
|
|
|
116
115
|
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
117
|
-
|
|
116
|
+
current = path.join(current, segment);
|
|
118
117
|
|
|
119
118
|
let stat: fs.Stats;
|
|
119
|
+
|
|
120
120
|
try {
|
|
121
|
-
stat = await fs.promises.lstat(
|
|
121
|
+
stat = await fs.promises.lstat(current);
|
|
122
122
|
} catch (cause) {
|
|
123
123
|
if ((cause as NodeJS.ErrnoException).code === 'ENOENT') break;
|
|
124
124
|
|
|
125
125
|
throw new HarmonyConfigPluginError(
|
|
126
126
|
'ERR_HARMONY_PATH_INVALID',
|
|
127
|
-
`Cannot inspect managed Harmony path ${
|
|
128
|
-
{ cause, file:
|
|
127
|
+
`Cannot inspect managed Harmony path ${current}: ${(cause as Error).message}`,
|
|
128
|
+
{ cause, file: current, operation: 'resolve-path' }
|
|
129
129
|
);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
if (stat.isSymbolicLink()) {
|
|
133
|
-
const real = await fs.promises.realpath(
|
|
133
|
+
const real = await fs.promises.realpath(current);
|
|
134
134
|
|
|
135
|
-
if (!isInside(
|
|
135
|
+
if (!isInside(canonical, real)) {
|
|
136
136
|
throw new HarmonyConfigPluginError(
|
|
137
137
|
'ERR_HARMONY_PATH_ESCAPE',
|
|
138
|
-
`Refusing to follow a symlink outside the Harmony project: ${
|
|
139
|
-
{ file:
|
|
138
|
+
`Refusing to follow a symlink outside the Harmony project: ${current}`,
|
|
139
|
+
{ file: current, operation: 'resolve-path' }
|
|
140
140
|
);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
@@ -144,24 +144,24 @@ export async function assertNoExternalSymlink(root: string, target: string): Pro
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
export async function resolveHarmonyPath(
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
directory: string,
|
|
148
|
+
relative: string
|
|
149
149
|
): Promise<string> {
|
|
150
|
-
if (typeof
|
|
150
|
+
if (typeof relative !== 'string' || !relative || path.isAbsolute(relative)) {
|
|
151
151
|
throw new HarmonyConfigPluginError(
|
|
152
152
|
'ERR_HARMONY_PATH_ESCAPE',
|
|
153
|
-
`Invalid managed path: ${
|
|
153
|
+
`Invalid managed path: ${relative}`,
|
|
154
154
|
{ operation: 'resolve-path' }
|
|
155
155
|
);
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
const root = path.resolve(
|
|
159
|
-
const target = path.resolve(root,
|
|
158
|
+
const root = path.resolve(directory);
|
|
159
|
+
const target = path.resolve(root, relative);
|
|
160
160
|
|
|
161
161
|
if (!isInside(root, target)) {
|
|
162
162
|
throw new HarmonyConfigPluginError(
|
|
163
163
|
'ERR_HARMONY_PATH_ESCAPE',
|
|
164
|
-
`Managed path escapes the Harmony project: ${
|
|
164
|
+
`Managed path escapes the Harmony project: ${relative}`,
|
|
165
165
|
{ file: target, operation: 'resolve-path' }
|
|
166
166
|
);
|
|
167
167
|
}
|
|
@@ -172,30 +172,26 @@ export async function resolveHarmonyPath(
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
export async function resolveProjectPath(
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
root: string,
|
|
176
|
+
name: keyof HarmonyProjectPaths
|
|
177
177
|
): Promise<string> {
|
|
178
|
-
const candidates = ProjectPathCandidates[
|
|
179
|
-
const resolved = await Promise.all(candidates.map(candidate => resolveHarmonyPath(projectRoot, candidate)));
|
|
180
|
-
const existing = resolved.filter(file => fs.existsSync(file));
|
|
178
|
+
const candidates = ProjectPathCandidates[name] || [ProjectPaths[name]];
|
|
181
179
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
'ERR_HARMONY_CONFIG_INVALID',
|
|
185
|
-
`Multiple files provide harmony.${modName}: ${existing.map(file => path.basename(file)).join(', ')}`,
|
|
186
|
-
{ file: existing[0], operation: `harmony.${modName}.read` }
|
|
187
|
-
);
|
|
180
|
+
for (const candidate of candidates) {
|
|
181
|
+
if (fs.existsSync(path.join(root, candidate))) return resolveHarmonyPath(root, candidate);
|
|
188
182
|
}
|
|
189
183
|
|
|
190
|
-
return
|
|
184
|
+
return resolveHarmonyPath(root, candidates[0]);
|
|
191
185
|
}
|
|
192
186
|
|
|
193
|
-
export function toPosixRelative(
|
|
194
|
-
return path.relative(
|
|
187
|
+
export function toPosixRelative(root: string, target: string): string {
|
|
188
|
+
return path.relative(root, target).split(path.sep).join('/');
|
|
195
189
|
}
|
|
196
190
|
|
|
197
|
-
|
|
198
|
-
|
|
191
|
+
const { nativeInputsStamp: NativeInputsStampPath, ...PublicManagedPaths } = ManagedPaths;
|
|
192
|
+
|
|
193
|
+
export const HarmonyPaths: Readonly<HarmonyPathsApi> = Object.freeze({
|
|
194
|
+
HARMONY_PATHS: Object.freeze(PublicManagedPaths),
|
|
199
195
|
PROJECT_PATH_CANDIDATES: ProjectPathCandidates,
|
|
200
196
|
PROJECT_PATHS: ProjectPaths,
|
|
201
197
|
RESOURCE_PATHS: ResourcePaths,
|
|
@@ -204,6 +200,6 @@ export const HarmonyPaths: HarmonyPathsApi = {
|
|
|
204
200
|
resolveHarmonyPath,
|
|
205
201
|
resolveProjectPath,
|
|
206
202
|
toPosixRelative,
|
|
207
|
-
};
|
|
203
|
+
});
|
|
208
204
|
|
|
209
|
-
export { ManagedPaths, ProjectPathCandidates, ProjectPaths, ResourcePaths };
|
|
205
|
+
export { ManagedPaths, NativeInputsStampPath, ProjectPathCandidates, ProjectPaths, ResourcePaths };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { HarmonyPermission } from './config';
|
|
2
|
+
import { getModuleOrThrow, type HarmonyModuleJson } from './manifest';
|
|
3
|
+
import { withModuleJson } from './mods';
|
|
4
|
+
import { normalizePermission } from './normalizeConfig';
|
|
5
|
+
import type { HarmonyConfigPlugin } from './pluginTypes';
|
|
6
|
+
|
|
7
|
+
export function mergePermissions(
|
|
8
|
+
current: readonly HarmonyPermission[],
|
|
9
|
+
additions: readonly HarmonyPermission[]
|
|
10
|
+
): HarmonyPermission[] {
|
|
11
|
+
const result = new Map<string, HarmonyPermission>();
|
|
12
|
+
for (const input of [...current, ...additions]) {
|
|
13
|
+
const permission = normalizePermission(input);
|
|
14
|
+
const existing = result.get(permission.name);
|
|
15
|
+
const scene = existing?.usedScene || permission.usedScene;
|
|
16
|
+
const abilities = [...new Set([
|
|
17
|
+
...(existing?.usedScene?.abilities ?? []),
|
|
18
|
+
...(permission.usedScene?.abilities ?? []),
|
|
19
|
+
])];
|
|
20
|
+
result.set(permission.name, {
|
|
21
|
+
...existing,
|
|
22
|
+
...permission,
|
|
23
|
+
...(scene
|
|
24
|
+
? {
|
|
25
|
+
usedScene: {
|
|
26
|
+
...(abilities.length ? { abilities } : {}),
|
|
27
|
+
when: existing?.usedScene?.when === 'always' || permission.usedScene?.when === 'always' ? 'always' : 'inuse',
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
: {}),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return [...result.values()];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function ensurePermissions(
|
|
38
|
+
manifest: HarmonyModuleJson,
|
|
39
|
+
permissions: readonly HarmonyPermission[]
|
|
40
|
+
): HarmonyModuleJson {
|
|
41
|
+
const module = getModuleOrThrow(manifest);
|
|
42
|
+
module.requestPermissions = mergePermissions(module.requestPermissions ?? [], permissions);
|
|
43
|
+
return manifest;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function removePermissions(manifest: HarmonyModuleJson, names: readonly string[]): HarmonyModuleJson {
|
|
47
|
+
const module = getModuleOrThrow(manifest);
|
|
48
|
+
if (module.requestPermissions) {
|
|
49
|
+
module.requestPermissions = module.requestPermissions.filter(permission => !names.includes(permission.name));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return manifest;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const withHarmonyPermissions: HarmonyConfigPlugin<readonly HarmonyPermission[]> = (config, permissions) => {
|
|
56
|
+
const additions = mergePermissions([], permissions);
|
|
57
|
+
if (config.harmony) {
|
|
58
|
+
config.harmony = { ...config.harmony, permissions: mergePermissions(config.harmony.permissions ?? [], additions) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return withModuleJson(config, (mod) => {
|
|
62
|
+
if (mod.modResults.module === undefined) mod.modResults.module = {};
|
|
63
|
+
ensurePermissions(mod.modResults, additions);
|
|
64
|
+
return mod;
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const HarmonyPermissions = Object.freeze({ ensurePermissions, mergePermissions, removePermissions });
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ExportedConfigWithProps } from '@expo/config-plugins';
|
|
2
|
+
|
|
3
|
+
import type { ExpoConfigWithHarmony, ExpoHarmonyPlatform, HarmonyConfig } from './config';
|
|
4
|
+
|
|
5
|
+
export type HarmonyConfigPlugin<Props = void> = <Config extends ExpoConfigWithHarmony>(
|
|
6
|
+
config: Config,
|
|
7
|
+
props: Props
|
|
8
|
+
) => Config;
|
|
9
|
+
|
|
10
|
+
export type HarmonyModConfig<T> = Omit<ExportedConfigWithProps<T>, 'platforms' | 'modRawConfig' | 'modRequest'> & {
|
|
11
|
+
platforms?: ExpoHarmonyPlatform[];
|
|
12
|
+
harmony?: HarmonyConfig;
|
|
13
|
+
modRawConfig: ExpoConfigWithHarmony;
|
|
14
|
+
modRequest: Omit<ExportedConfigWithProps<T>['modRequest'], 'platform' | 'nextMod'> & {
|
|
15
|
+
platform: 'harmony';
|
|
16
|
+
nextMod?: HarmonyModAction<T>;
|
|
17
|
+
modFile?: string;
|
|
18
|
+
modFileExists?: boolean;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type HarmonyModAction<T = unknown> = (
|
|
23
|
+
config: HarmonyModConfig<T>
|
|
24
|
+
) => HarmonyModConfig<T> | Promise<HarmonyModConfig<T>>;
|