@expo-harmony/config-plugins 55.0.10-harmony.2 → 55.0.10-harmony.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +203 -11
- package/build/compiler.d.ts +7 -0
- package/build/compiler.js +12 -0
- package/build/composition.d.ts +24 -0
- package/build/composition.js +11 -0
- package/build/config.d.ts +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 +52 -6
- package/build/internal.d.ts +3 -0
- package/build/internal.js +13 -0
- package/build/manifest.d.ts +38 -0
- package/build/manifest.js +71 -0
- package/build/mods.d.ts +36 -31
- package/build/mods.js +114 -86
- package/build/normalizeConfig.d.ts +10 -3
- package/build/normalizeConfig.js +41 -47
- package/build/ownership.d.ts +4 -3
- package/build/ownership.js +17 -0
- package/build/paths.d.ts +12 -10
- package/build/paths.js +31 -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 +19 -11
- package/src/internal.ts +3 -0
- package/src/manifest.ts +151 -0
- package/src/mods.ts +170 -129
- package/src/nativeInputs.ts +0 -1
- package/src/normalizeConfig.ts +51 -59
- package/src/ownership.ts +25 -5
- package/src/paths.ts +38 -46
- package/src/permissions.ts +68 -0
- package/src/pluginTypes.ts +24 -0
- package/src/resources.ts +65 -0
package/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 = 13;
|
|
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
|
}
|
|
@@ -359,6 +350,7 @@ function normalizeHarmonyConfig(config: HarmonyExpoConfig): NormalizedHarmonyCon
|
|
|
359
350
|
}
|
|
360
351
|
|
|
361
352
|
skills = [...new Map(skills.map(skill => [JSON.stringify(skill), skill])).values()];
|
|
353
|
+
|
|
362
354
|
const queries = [...new Set([
|
|
363
355
|
'http',
|
|
364
356
|
'https',
|
|
@@ -378,40 +370,40 @@ function normalizeHarmonyConfig(config: HarmonyExpoConfig): NormalizedHarmonyCon
|
|
|
378
370
|
: normalizeStringArray<HarmonyDeviceType>(
|
|
379
371
|
harmony.deviceTypes,
|
|
380
372
|
'harmony.deviceTypes',
|
|
381
|
-
|
|
373
|
+
DeviceTypes
|
|
382
374
|
);
|
|
383
375
|
|
|
384
376
|
return Object.freeze({
|
|
385
377
|
abiFilters: abis,
|
|
386
|
-
abilityName,
|
|
378
|
+
abilityName: ability,
|
|
387
379
|
backgroundColor: background,
|
|
388
|
-
bundleName,
|
|
380
|
+
bundleName: bundle,
|
|
389
381
|
compatibleSdkVersionString: resolveSdkVersion(
|
|
390
|
-
|
|
382
|
+
compatible,
|
|
391
383
|
'compatibleSdkVersion',
|
|
392
384
|
harmony.compatibleSdkVersion
|
|
393
385
|
),
|
|
394
386
|
deviceTypes: devices,
|
|
395
387
|
icon: harmony.icon || config.icon,
|
|
396
388
|
label: readString(harmony.label, 'harmony.label', config.name),
|
|
397
|
-
moduleName,
|
|
389
|
+
moduleName: module,
|
|
398
390
|
nativeOrientation: orientation === 'default' ? 'unspecified' : orientation,
|
|
399
391
|
permissions,
|
|
400
392
|
productName: readString(harmony.productName, 'harmony.productName', 'default'),
|
|
401
393
|
querySchemes: queries,
|
|
402
394
|
signingConfigFile: signing,
|
|
403
395
|
skills,
|
|
404
|
-
targetApiVersion:
|
|
396
|
+
targetApiVersion: target,
|
|
405
397
|
targetSdkVersionString: resolveSdkVersion(
|
|
406
|
-
|
|
398
|
+
target,
|
|
407
399
|
'targetSdkVersion',
|
|
408
400
|
harmony.targetSdkVersion
|
|
409
401
|
),
|
|
410
402
|
vendor: readString(harmony.vendor, 'harmony.vendor', 'expo-harmony'),
|
|
411
|
-
versionCode,
|
|
412
|
-
versionName,
|
|
403
|
+
versionCode: code,
|
|
404
|
+
versionName: version,
|
|
413
405
|
});
|
|
414
406
|
}
|
|
415
407
|
|
|
416
|
-
export {
|
|
408
|
+
export { normalizeHarmonyConfig };
|
|
417
409
|
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,7 +8,6 @@ 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;
|
|
@@ -44,12 +43,12 @@ export interface HarmonyPathsApi {
|
|
|
44
43
|
readonly RESOURCE_PATHS: HarmonyResourcePaths;
|
|
45
44
|
assertNoExternalSymlink(root: string, target: string): Promise<void>;
|
|
46
45
|
isInside(root: string, target: string): boolean;
|
|
47
|
-
resolveHarmonyPath(
|
|
48
|
-
resolveProjectPath(
|
|
49
|
-
toPosixRelative(
|
|
46
|
+
resolveHarmonyPath(directory: string, relative: string): Promise<string>;
|
|
47
|
+
resolveProjectPath(root: string, name: keyof HarmonyProjectPaths): Promise<string>;
|
|
48
|
+
toPosixRelative(root: string, target: string): string;
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
const ManagedPaths: HarmonyManagedPaths = Object.freeze({
|
|
51
|
+
const ManagedPaths: HarmonyManagedPaths & { readonly nativeInputsStamp: string } = Object.freeze({
|
|
53
52
|
appJson: 'AppScope/app.json5',
|
|
54
53
|
projectBuildProfile: 'build-profile.json5',
|
|
55
54
|
rootOhPackage: 'oh-package.json5',
|
|
@@ -74,8 +73,7 @@ const ProjectPaths: HarmonyProjectPaths = Object.freeze({
|
|
|
74
73
|
});
|
|
75
74
|
|
|
76
75
|
const ProjectPathCandidates: HarmonyProjectPathCandidates = Object.freeze({
|
|
77
|
-
//
|
|
78
|
-
// search places. The first entry is also the file created for new projects.
|
|
76
|
+
// Match React Native CLI lookup precedence; new projects use the first path.
|
|
79
77
|
reactNativeConfig: Object.freeze([
|
|
80
78
|
'react-native.config.js',
|
|
81
79
|
'react-native.config.cjs',
|
|
@@ -102,41 +100,40 @@ const ResourcePaths: HarmonyResourcePaths = Object.freeze({
|
|
|
102
100
|
|
|
103
101
|
export function isInside(root: string, target: string): boolean {
|
|
104
102
|
const relative = path.relative(root, target);
|
|
105
|
-
|
|
106
103
|
return relative === ''
|
|
107
104
|
|| (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
108
105
|
}
|
|
109
106
|
|
|
110
107
|
export async function assertNoExternalSymlink(root: string, target: string): Promise<void> {
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const relative = path.relative(
|
|
114
|
-
let
|
|
108
|
+
const absolute = path.resolve(root);
|
|
109
|
+
const canonical = await fs.promises.realpath(absolute).catch(() => absolute);
|
|
110
|
+
const relative = path.relative(absolute, target);
|
|
111
|
+
let current = absolute;
|
|
115
112
|
|
|
116
113
|
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
117
|
-
|
|
114
|
+
current = path.join(current, segment);
|
|
118
115
|
|
|
119
116
|
let stat: fs.Stats;
|
|
120
117
|
try {
|
|
121
|
-
stat = await fs.promises.lstat(
|
|
118
|
+
stat = await fs.promises.lstat(current);
|
|
122
119
|
} catch (cause) {
|
|
123
120
|
if ((cause as NodeJS.ErrnoException).code === 'ENOENT') break;
|
|
124
121
|
|
|
125
122
|
throw new HarmonyConfigPluginError(
|
|
126
123
|
'ERR_HARMONY_PATH_INVALID',
|
|
127
|
-
`Cannot inspect managed Harmony path ${
|
|
128
|
-
{ cause, file:
|
|
124
|
+
`Cannot inspect managed Harmony path ${current}: ${(cause as Error).message}`,
|
|
125
|
+
{ cause, file: current, operation: 'resolve-path' }
|
|
129
126
|
);
|
|
130
127
|
}
|
|
131
128
|
|
|
132
129
|
if (stat.isSymbolicLink()) {
|
|
133
|
-
const real = await fs.promises.realpath(
|
|
130
|
+
const real = await fs.promises.realpath(current);
|
|
134
131
|
|
|
135
|
-
if (!isInside(
|
|
132
|
+
if (!isInside(canonical, real)) {
|
|
136
133
|
throw new HarmonyConfigPluginError(
|
|
137
134
|
'ERR_HARMONY_PATH_ESCAPE',
|
|
138
|
-
`Refusing to follow a symlink outside the Harmony project: ${
|
|
139
|
-
{ file:
|
|
135
|
+
`Refusing to follow a symlink outside the Harmony project: ${current}`,
|
|
136
|
+
{ file: current, operation: 'resolve-path' }
|
|
140
137
|
);
|
|
141
138
|
}
|
|
142
139
|
}
|
|
@@ -144,24 +141,24 @@ export async function assertNoExternalSymlink(root: string, target: string): Pro
|
|
|
144
141
|
}
|
|
145
142
|
|
|
146
143
|
export async function resolveHarmonyPath(
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
directory: string,
|
|
145
|
+
relative: string
|
|
149
146
|
): Promise<string> {
|
|
150
|
-
if (typeof
|
|
147
|
+
if (typeof relative !== 'string' || !relative || path.isAbsolute(relative)) {
|
|
151
148
|
throw new HarmonyConfigPluginError(
|
|
152
149
|
'ERR_HARMONY_PATH_ESCAPE',
|
|
153
|
-
`Invalid managed path: ${
|
|
150
|
+
`Invalid managed path: ${relative}`,
|
|
154
151
|
{ operation: 'resolve-path' }
|
|
155
152
|
);
|
|
156
153
|
}
|
|
157
154
|
|
|
158
|
-
const root = path.resolve(
|
|
159
|
-
const target = path.resolve(root,
|
|
155
|
+
const root = path.resolve(directory);
|
|
156
|
+
const target = path.resolve(root, relative);
|
|
160
157
|
|
|
161
158
|
if (!isInside(root, target)) {
|
|
162
159
|
throw new HarmonyConfigPluginError(
|
|
163
160
|
'ERR_HARMONY_PATH_ESCAPE',
|
|
164
|
-
`Managed path escapes the Harmony project: ${
|
|
161
|
+
`Managed path escapes the Harmony project: ${relative}`,
|
|
165
162
|
{ file: target, operation: 'resolve-path' }
|
|
166
163
|
);
|
|
167
164
|
}
|
|
@@ -172,30 +169,25 @@ export async function resolveHarmonyPath(
|
|
|
172
169
|
}
|
|
173
170
|
|
|
174
171
|
export async function resolveProjectPath(
|
|
175
|
-
|
|
176
|
-
|
|
172
|
+
root: string,
|
|
173
|
+
name: keyof HarmonyProjectPaths
|
|
177
174
|
): Promise<string> {
|
|
178
|
-
const candidates = ProjectPathCandidates[
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (existing.length > 1) {
|
|
183
|
-
throw new HarmonyConfigPluginError(
|
|
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
|
-
);
|
|
175
|
+
const candidates = ProjectPathCandidates[name] || [ProjectPaths[name]];
|
|
176
|
+
for (const candidate of candidates) {
|
|
177
|
+
if (fs.existsSync(path.join(root, candidate))) return resolveHarmonyPath(root, candidate);
|
|
188
178
|
}
|
|
189
179
|
|
|
190
|
-
return
|
|
180
|
+
return resolveHarmonyPath(root, candidates[0]);
|
|
191
181
|
}
|
|
192
182
|
|
|
193
|
-
export function toPosixRelative(
|
|
194
|
-
return path.relative(
|
|
183
|
+
export function toPosixRelative(root: string, target: string): string {
|
|
184
|
+
return path.relative(root, target).split(path.sep).join('/');
|
|
195
185
|
}
|
|
196
186
|
|
|
197
|
-
|
|
198
|
-
|
|
187
|
+
const { nativeInputsStamp: NativeInputsStampPath, ...PublicManagedPaths } = ManagedPaths;
|
|
188
|
+
|
|
189
|
+
export const HarmonyPaths: Readonly<HarmonyPathsApi> = Object.freeze({
|
|
190
|
+
HARMONY_PATHS: Object.freeze(PublicManagedPaths),
|
|
199
191
|
PROJECT_PATH_CANDIDATES: ProjectPathCandidates,
|
|
200
192
|
PROJECT_PATHS: ProjectPaths,
|
|
201
193
|
RESOURCE_PATHS: ResourcePaths,
|
|
@@ -204,6 +196,6 @@ export const HarmonyPaths: HarmonyPathsApi = {
|
|
|
204
196
|
resolveHarmonyPath,
|
|
205
197
|
resolveProjectPath,
|
|
206
198
|
toPosixRelative,
|
|
207
|
-
};
|
|
199
|
+
});
|
|
208
200
|
|
|
209
|
-
export { ManagedPaths, ProjectPathCandidates, ProjectPaths, ResourcePaths };
|
|
201
|
+
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>>;
|
package/src/resources.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { HarmonyConfigPluginError } from './errors';
|
|
2
|
+
|
|
3
|
+
export interface HarmonyResourceItem extends Record<string, unknown> {
|
|
4
|
+
name: string;
|
|
5
|
+
value: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface HarmonyResourceFile extends Record<string, unknown> {
|
|
9
|
+
string?: HarmonyResourceItem[];
|
|
10
|
+
color?: HarmonyResourceItem[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function setItem(file: HarmonyResourceFile, kind: 'string' | 'color', item: HarmonyResourceItem): HarmonyResourceFile {
|
|
14
|
+
if (!item || typeof item.name !== 'string' || !/^[A-Za-z0-9_.-]+$/.test(item.name)
|
|
15
|
+
|| item.name.includes('..') || typeof item.value !== 'string') {
|
|
16
|
+
throw new HarmonyConfigPluginError('ERR_HARMONY_RESOURCE_INVALID', `Invalid Harmony ${kind} resource.`, { operation: 'edit-resource' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const items = readItems(file, kind);
|
|
20
|
+
const existing = items.find(value => value?.name === item.name);
|
|
21
|
+
file[kind] = [...items.filter(value => value?.name !== item.name), { ...existing, ...item }];
|
|
22
|
+
|
|
23
|
+
return file;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readItems(file: HarmonyResourceFile, kind: 'string' | 'color'): HarmonyResourceItem[] {
|
|
27
|
+
if (file[kind] !== undefined && !Array.isArray(file[kind])) {
|
|
28
|
+
throw new HarmonyConfigPluginError('ERR_HARMONY_RESOURCE_INVALID', `Harmony ${kind} resources must be an array.`, { operation: 'edit-resource' });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return file[kind] ?? [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function setString(file: HarmonyResourceFile, item: HarmonyResourceItem): HarmonyResourceFile {
|
|
35
|
+
return setItem(file, 'string', item);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function removeString(file: HarmonyResourceFile, name: string): HarmonyResourceFile {
|
|
39
|
+
const items = readItems(file, 'string');
|
|
40
|
+
if (file.string) file.string = items.filter(item => item?.name !== name);
|
|
41
|
+
return file;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function setColor(file: HarmonyResourceFile, item: HarmonyResourceItem): HarmonyResourceFile {
|
|
45
|
+
return setItem(file, 'color', item);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function removeColor(file: HarmonyResourceFile, name: string): HarmonyResourceFile {
|
|
49
|
+
const items = readItems(file, 'color');
|
|
50
|
+
if (file.color) file.color = items.filter(item => item?.name !== name);
|
|
51
|
+
return file;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function toArgb(value: string | number): string {
|
|
55
|
+
let hex: string;
|
|
56
|
+
if (typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 0xffffffff) {
|
|
57
|
+
hex = value.toString(16).padStart(8, '0');
|
|
58
|
+
} else if (typeof value === 'string' && /^#[0-9a-f]{6}(?:[0-9a-f]{2})?$/i.test(value)) {
|
|
59
|
+
hex = value.slice(1);
|
|
60
|
+
} else {
|
|
61
|
+
throw new HarmonyConfigPluginError('ERR_HARMONY_CONFIG_INVALID', 'Color must be #RRGGBB, #RRGGBBAA or an unsigned RGBA integer.', { operation: 'convert-color' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return `#${hex.length === 8 ? hex.slice(6, 8) + hex.slice(0, 6) : hex}`.toUpperCase();
|
|
65
|
+
}
|