@ankhorage/expo-runtime 0.0.0 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +3 -0
  3. package/dist/ExpoBarcodeScannerAdapter.d.ts +4 -0
  4. package/dist/ExpoBarcodeScannerAdapter.d.ts.map +1 -0
  5. package/dist/ExpoBarcodeScannerAdapter.js +49 -0
  6. package/dist/ExpoBarcodeScannerAdapter.js.map +1 -0
  7. package/dist/ExpoRuntimeProviders.d.ts +8 -0
  8. package/dist/ExpoRuntimeProviders.d.ts.map +1 -0
  9. package/dist/ExpoRuntimeProviders.js +11 -0
  10. package/dist/ExpoRuntimeProviders.js.map +1 -0
  11. package/dist/barcodeScanRuntime.d.ts +15 -0
  12. package/dist/barcodeScanRuntime.d.ts.map +1 -0
  13. package/dist/barcodeScanRuntime.js +48 -0
  14. package/dist/barcodeScanRuntime.js.map +1 -0
  15. package/dist/componentRegistry.d.ts +4 -0
  16. package/dist/componentRegistry.d.ts.map +1 -0
  17. package/dist/componentRegistry.js +6 -0
  18. package/dist/componentRegistry.js.map +1 -0
  19. package/dist/createExpoRuntimeRegistry.d.ts +3 -0
  20. package/dist/createExpoRuntimeRegistry.d.ts.map +1 -0
  21. package/dist/createExpoRuntimeRegistry.js +14 -0
  22. package/dist/createExpoRuntimeRegistry.js.map +1 -0
  23. package/dist/index.d.ts +8 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +6 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/permissionRuntime.d.ts +3 -0
  28. package/dist/permissionRuntime.d.ts.map +1 -0
  29. package/dist/permissionRuntime.js +3 -0
  30. package/dist/permissionRuntime.js.map +1 -0
  31. package/dist/resolveExpoRuntimePlan.d.ts +53 -0
  32. package/dist/resolveExpoRuntimePlan.d.ts.map +1 -0
  33. package/dist/resolveExpoRuntimePlan.js +215 -0
  34. package/dist/resolveExpoRuntimePlan.js.map +1 -0
  35. package/package.json +19 -4
  36. package/src/ExpoBarcodeScannerAdapter.test.tsx +75 -0
  37. package/src/ExpoBarcodeScannerAdapter.tsx +84 -0
  38. package/src/ExpoRuntimeProviders.test.tsx +61 -0
  39. package/src/ExpoRuntimeProviders.tsx +21 -0
  40. package/src/barcodeScanRuntime.ts +78 -0
  41. package/src/componentRegistry.ts +11 -0
  42. package/src/createExpoRuntimeRegistry.test.tsx +31 -0
  43. package/src/createExpoRuntimeRegistry.tsx +17 -0
  44. package/src/index.ts +20 -1
  45. package/src/permissionRuntime.ts +2 -0
  46. package/src/resolveExpoRuntimePlan.test.ts +212 -0
  47. package/src/resolveExpoRuntimePlan.ts +337 -0
@@ -0,0 +1,212 @@
1
+ import type { AppManifest } from '@ankhorage/contracts';
2
+ import { Permission } from '@ankhorage/permissions';
3
+ import { EXPO_PERMISSION_SUPPORT } from '@ankhorage/permissions/expo/manifest';
4
+ import { describe, expect, test } from 'bun:test';
5
+
6
+ import { resolveExpoRuntimePlan } from './resolveExpoRuntimePlan';
7
+
8
+ describe('resolveExpoRuntimePlan', () => {
9
+ test('resolves camera permission through Expo permission metadata', () => {
10
+ const plan = resolveExpoRuntimePlan(
11
+ withFirstScreenRequirements({
12
+ permissions: [{ permission: 'camera' }],
13
+ }),
14
+ );
15
+
16
+ expect(plan.dependencies.map((dependency) => dependency.name)).toEqual([
17
+ '@ankhorage/expo-runtime',
18
+ '@ankhorage/permissions',
19
+ 'expo-camera',
20
+ ]);
21
+ expect(plan.providers).toEqual(['permissions']);
22
+ expect(plan.needsPermissionsProvider).toBe(true);
23
+ expect(plan.nativeConfig.configHints).toEqual(['cameraPermission']);
24
+ expect(plan.nativeConfig.androidPermissions).toEqual(['android.permission.CAMERA']);
25
+ expect(plan.nativeConfig.plugins).toEqual([
26
+ {
27
+ name: 'expo-camera',
28
+ options: {
29
+ cameraPermission: 'Allow camera access.',
30
+ },
31
+ },
32
+ ]);
33
+ });
34
+
35
+ test('resolves barcodeScanner capability with implied camera permission and adapter wiring', () => {
36
+ const plan = resolveExpoRuntimePlan(
37
+ withFirstScreenRequirements({
38
+ capabilities: [{ capability: 'barcodeScanner' }],
39
+ }),
40
+ );
41
+
42
+ expect(plan.impliedPermissions).toEqual([{ permission: 'camera' }]);
43
+ expect(plan.dependencies.map((dependency) => dependency.name)).toEqual([
44
+ '@ankhorage/expo-runtime',
45
+ '@ankhorage/permissions',
46
+ 'expo-camera',
47
+ ]);
48
+ expect(plan.runtimeAdapters).toEqual(['ExpoBarcodeScannerAdapter']);
49
+ expect(plan.usesExpoRuntimeRegistry).toBe(true);
50
+ expect(plan.nativeConfig.plugins).toEqual([
51
+ {
52
+ name: 'expo-camera',
53
+ options: {
54
+ cameraPermission: 'Allow camera access to scan barcodes.',
55
+ },
56
+ },
57
+ ]);
58
+ });
59
+
60
+ test('dedupes repeated permission and capability requirements', () => {
61
+ const plan = resolveExpoRuntimePlan(
62
+ withAllScreenRequirements({
63
+ capabilities: [{ capability: 'barcodeScanner' }],
64
+ permissions: [{ permission: 'camera' }],
65
+ }),
66
+ );
67
+
68
+ expect(plan.permissions).toEqual([{ permission: 'camera' }]);
69
+ expect(plan.capabilities).toEqual([{ capability: 'barcodeScanner' }]);
70
+ expect(plan.dependencies.map((dependency) => dependency.name)).toEqual([
71
+ '@ankhorage/expo-runtime',
72
+ '@ankhorage/permissions',
73
+ 'expo-camera',
74
+ ]);
75
+ });
76
+
77
+ test('surfaces unsupported permission support explicitly', () => {
78
+ const plan = resolveExpoRuntimePlan(
79
+ withFirstScreenRequirements({
80
+ permissions: [{ permission: 'camera' }],
81
+ }),
82
+ {
83
+ permissionSupport: {
84
+ ...EXPO_PERMISSION_SUPPORT,
85
+ [Permission.Camera]: {
86
+ ...EXPO_PERMISSION_SUPPORT[Permission.Camera],
87
+ support: 'unsupported',
88
+ },
89
+ },
90
+ },
91
+ );
92
+
93
+ expect(plan.diagnostics).toContainEqual({
94
+ severity: 'error',
95
+ requirementType: 'permission',
96
+ requirement: 'camera',
97
+ message: "Expo runtime support for 'camera' is 'unsupported'.",
98
+ support: 'unsupported',
99
+ });
100
+ expect(plan.dependencies).toEqual([]);
101
+ expect(plan.providers).toEqual([]);
102
+ });
103
+
104
+ test('does not add runtime packages or providers for clipboard permissions', () => {
105
+ const plan = resolveExpoRuntimePlan(
106
+ withFirstScreenRequirements({
107
+ permissions: [{ permission: 'clipboard' }],
108
+ }),
109
+ );
110
+
111
+ expect(plan.dependencies).toEqual([]);
112
+ expect(plan.providers).toEqual([]);
113
+ expect(plan.nativeConfig.plugins).toEqual([]);
114
+ });
115
+ });
116
+
117
+ function withAllScreenRequirements(requirements: TestScreenRequirements): AppManifest {
118
+ return {
119
+ ...BASE_MANIFEST,
120
+ screens: {
121
+ home: {
122
+ ...BASE_MANIFEST.screens.home,
123
+ requires: requirements,
124
+ },
125
+ settings: {
126
+ ...BASE_MANIFEST.screens.settings,
127
+ requires: requirements,
128
+ },
129
+ },
130
+ };
131
+ }
132
+
133
+ function withFirstScreenRequirements(requirements: TestScreenRequirements): AppManifest {
134
+ return {
135
+ ...BASE_MANIFEST,
136
+ screens: {
137
+ ...BASE_MANIFEST.screens,
138
+ home: {
139
+ ...BASE_MANIFEST.screens.home,
140
+ requires: requirements,
141
+ },
142
+ },
143
+ };
144
+ }
145
+
146
+ interface TestScreenRequirements {
147
+ readonly capabilities?: readonly [{ readonly capability: 'barcodeScanner' }];
148
+ readonly permissions?: readonly [{ readonly permission: 'camera' | 'clipboard' }];
149
+ }
150
+
151
+ const BASE_MANIFEST = {
152
+ metadata: {
153
+ name: 'Test App',
154
+ slug: 'test-app',
155
+ version: '1.0.0',
156
+ themeId: 'default',
157
+ created: '2026-06-10T00:00:00.000Z',
158
+ updated: '2026-06-10T00:00:00.000Z',
159
+ },
160
+ navigator: {
161
+ type: 'stack',
162
+ routes: [
163
+ { name: 'index', screenId: 'home' },
164
+ { name: 'settings', screenId: 'settings' },
165
+ ],
166
+ },
167
+ screens: {
168
+ home: {
169
+ id: 'home',
170
+ name: 'Home',
171
+ title: 'Home',
172
+ root: { id: 'home-root', type: 'Screen' },
173
+ },
174
+ settings: {
175
+ id: 'settings',
176
+ name: 'Settings',
177
+ title: 'Settings',
178
+ root: { id: 'settings-root', type: 'Screen' },
179
+ },
180
+ },
181
+ themes: [
182
+ {
183
+ id: 'default',
184
+ name: 'Default',
185
+ light: {
186
+ primaryColor: '#0f172a',
187
+ harmony: 'analogous',
188
+ },
189
+ dark: {
190
+ primaryColor: '#0f172a',
191
+ harmony: 'analogous',
192
+ },
193
+ },
194
+ ],
195
+ activeThemeId: 'default',
196
+ activeThemeMode: 'light',
197
+ infra: {
198
+ plugins: [],
199
+ },
200
+ settings: {
201
+ localization: {
202
+ defaultLocale: 'en',
203
+ locales: ['en'],
204
+ },
205
+ authFlow: {
206
+ signInRoute: '',
207
+ signUpRoute: '/sign-up',
208
+ signOutRoute: '/sign-out',
209
+ postSignInRoute: '/',
210
+ },
211
+ },
212
+ } satisfies AppManifest;
@@ -0,0 +1,337 @@
1
+ import type {
2
+ AnkhorageCapabilityName,
3
+ AppManifest,
4
+ ScreenCapabilityRequirement,
5
+ ScreenPermissionRequirement,
6
+ } from '@ankhorage/contracts';
7
+ import type { Permission } from '@ankhorage/permissions';
8
+ import { isPermission } from '@ankhorage/permissions';
9
+ import {
10
+ EXPO_PERMISSION_SUPPORT,
11
+ type ExpoPermissionMetadata,
12
+ type PermissionSupport,
13
+ } from '@ankhorage/permissions/expo/manifest';
14
+
15
+ interface ExpoRuntimeDependency {
16
+ readonly name: string;
17
+ readonly version: string;
18
+ readonly reasons: readonly string[];
19
+ }
20
+
21
+ interface ExpoRuntimeConfigPlugin {
22
+ readonly name: string;
23
+ readonly options?: Readonly<Record<string, boolean | string>>;
24
+ }
25
+
26
+ export type ExpoRuntimeProviderId = 'permissions';
27
+ export type ExpoRuntimeAdapterId = 'ExpoBarcodeScannerAdapter';
28
+
29
+ interface ExpoRuntimeDiagnostic {
30
+ readonly severity: 'error' | 'warning';
31
+ readonly requirementType: 'capability' | 'configHint' | 'package' | 'permission';
32
+ readonly requirement: string;
33
+ readonly message: string;
34
+ readonly support?: PermissionSupport;
35
+ }
36
+
37
+ export interface ExpoRuntimePlan {
38
+ readonly permissions: readonly ScreenPermissionRequirement[];
39
+ readonly capabilities: readonly ScreenCapabilityRequirement[];
40
+ readonly impliedPermissions: readonly ScreenPermissionRequirement[];
41
+ readonly dependencies: readonly ExpoRuntimeDependency[];
42
+ readonly nativeConfig: {
43
+ readonly androidPermissions: readonly string[];
44
+ readonly configHints: readonly string[];
45
+ readonly plugins: readonly ExpoRuntimeConfigPlugin[];
46
+ };
47
+ readonly providers: readonly ExpoRuntimeProviderId[];
48
+ readonly runtimeAdapters: readonly ExpoRuntimeAdapterId[];
49
+ readonly usesExpoRuntimeRegistry: boolean;
50
+ readonly needsPermissionsProvider: boolean;
51
+ readonly diagnostics: readonly ExpoRuntimeDiagnostic[];
52
+ }
53
+
54
+ interface ExpoRuntimeCapabilityMetadata {
55
+ readonly impliedPermissions?: readonly ScreenPermissionRequirement[];
56
+ readonly requiredPackages?: readonly string[];
57
+ readonly providers?: readonly ExpoRuntimeProviderId[];
58
+ readonly runtimeAdapters?: readonly ExpoRuntimeAdapterId[];
59
+ readonly androidPermissions?: readonly string[];
60
+ readonly plugins?: readonly ExpoRuntimeConfigPlugin[];
61
+ }
62
+
63
+ interface ExpoRuntimeHintMetadata {
64
+ readonly androidPermissions?: readonly string[];
65
+ readonly plugin?: ExpoRuntimeConfigPlugin;
66
+ }
67
+
68
+ interface ResolveExpoRuntimePlanOptions {
69
+ readonly capabilityRegistry?: Readonly<
70
+ Partial<Record<AnkhorageCapabilityName, ExpoRuntimeCapabilityMetadata>>
71
+ >;
72
+ readonly dependencyVersions?: Readonly<Record<string, string>>;
73
+ readonly permissionSupport?: Readonly<Record<Permission, ExpoPermissionMetadata>>;
74
+ }
75
+
76
+ const CAMERA_ANDROID_PERMISSION = 'android.permission.CAMERA';
77
+ const EXPO_RUNTIME_PACKAGE_NAME = '@ankhorage/expo-runtime';
78
+
79
+ const GENERATED_EXPO_DEPENDENCY_VERSIONS = {
80
+ '@ankhorage/permissions': '^0.2.0',
81
+ '@ankhorage/expo-runtime': 'latest',
82
+ 'expo-camera': '~17.0.10',
83
+ } as const satisfies Record<string, string>;
84
+
85
+ const EXPO_RUNTIME_PROVIDER_PACKAGES = {
86
+ permissions: '@ankhorage/permissions',
87
+ } as const satisfies Record<ExpoRuntimeProviderId, string>;
88
+
89
+ const EXPO_RUNTIME_CONFIG_HINTS = {
90
+ cameraPermission: {
91
+ androidPermissions: [CAMERA_ANDROID_PERMISSION],
92
+ plugin: {
93
+ name: 'expo-camera',
94
+ options: {
95
+ cameraPermission: 'Allow camera access.',
96
+ },
97
+ },
98
+ },
99
+ } as const satisfies Readonly<Record<string, ExpoRuntimeHintMetadata>>;
100
+
101
+ const EXPO_CAPABILITY_RUNTIME_REGISTRY = {
102
+ barcodeScanner: {
103
+ impliedPermissions: [{ permission: 'camera' }],
104
+ requiredPackages: ['expo-camera'],
105
+ providers: ['permissions'],
106
+ runtimeAdapters: ['ExpoBarcodeScannerAdapter'],
107
+ androidPermissions: [CAMERA_ANDROID_PERMISSION],
108
+ plugins: [
109
+ {
110
+ name: 'expo-camera',
111
+ options: {
112
+ cameraPermission: 'Allow camera access to scan barcodes.',
113
+ },
114
+ },
115
+ ],
116
+ },
117
+ } as const satisfies Readonly<
118
+ Partial<Record<AnkhorageCapabilityName, ExpoRuntimeCapabilityMetadata>>
119
+ >;
120
+
121
+ export function resolveExpoRuntimePlan(
122
+ manifest: AppManifest,
123
+ options: ResolveExpoRuntimePlanOptions = {},
124
+ ): ExpoRuntimePlan {
125
+ const permissionsByName = new Map<string, ScreenPermissionRequirement>();
126
+ const impliedPermissionsByName = new Map<string, ScreenPermissionRequirement>();
127
+ const capabilitiesByName = new Map<string, ScreenCapabilityRequirement>();
128
+
129
+ for (const screen of Object.values(manifest.screens)) {
130
+ screen.requires?.permissions?.forEach((requirement) => {
131
+ permissionsByName.set(requirement.permission, requirement);
132
+ });
133
+ screen.requires?.capabilities?.forEach((requirement) => {
134
+ capabilitiesByName.set(requirement.capability, requirement);
135
+ });
136
+ }
137
+
138
+ const capabilityRegistry: Readonly<
139
+ Partial<Record<AnkhorageCapabilityName, ExpoRuntimeCapabilityMetadata>>
140
+ > = options.capabilityRegistry ?? EXPO_CAPABILITY_RUNTIME_REGISTRY;
141
+
142
+ for (const capabilityRequirement of capabilitiesByName.values()) {
143
+ capabilityRegistry[capabilityRequirement.capability]?.impliedPermissions?.forEach(
144
+ (permissionRequirement: ScreenPermissionRequirement) => {
145
+ if (!permissionsByName.has(permissionRequirement.permission)) {
146
+ permissionsByName.set(permissionRequirement.permission, permissionRequirement);
147
+ impliedPermissionsByName.set(permissionRequirement.permission, permissionRequirement);
148
+ }
149
+ },
150
+ );
151
+ }
152
+
153
+ const diagnostics: ExpoRuntimeDiagnostic[] = [];
154
+ const dependencies = new Map<string, ExpoRuntimeDependency>();
155
+ const pluginOptions = new Map<string, Record<string, boolean | string>>();
156
+ const configHints = new Set<string>();
157
+ const androidPermissions = new Set<string>();
158
+ const providers = new Set<ExpoRuntimeProviderId>();
159
+ const runtimeAdapters = new Set<ExpoRuntimeAdapterId>();
160
+ const dependencyVersions: Readonly<Record<string, string>> =
161
+ options.dependencyVersions ?? GENERATED_EXPO_DEPENDENCY_VERSIONS;
162
+ const permissionSupport = options.permissionSupport ?? EXPO_PERMISSION_SUPPORT;
163
+ const configHintRegistry: Readonly<Record<string, ExpoRuntimeHintMetadata>> =
164
+ EXPO_RUNTIME_CONFIG_HINTS;
165
+
166
+ const addDependency = (name: string, reason: string) => {
167
+ const version = dependencyVersions[name];
168
+ if (version === undefined) {
169
+ diagnostics.push({
170
+ severity: 'warning',
171
+ requirementType: 'package',
172
+ requirement: name,
173
+ message: `No generated-app dependency version is registered for '${name}'.`,
174
+ });
175
+ return;
176
+ }
177
+
178
+ const existing = dependencies.get(name);
179
+ if (existing) {
180
+ dependencies.set(name, {
181
+ ...existing,
182
+ reasons: Array.from(new Set([...existing.reasons, reason])),
183
+ });
184
+ return;
185
+ }
186
+
187
+ dependencies.set(name, {
188
+ name,
189
+ version,
190
+ reasons: [reason],
191
+ });
192
+ };
193
+
194
+ const addProvider = (provider: ExpoRuntimeProviderId, reason: string) => {
195
+ providers.add(provider);
196
+ addDependency(EXPO_RUNTIME_PROVIDER_PACKAGES[provider], reason);
197
+ };
198
+
199
+ const addPlugin = (plugin: ExpoRuntimeConfigPlugin) => {
200
+ const existingOptions = pluginOptions.get(plugin.name) ?? {};
201
+ pluginOptions.set(plugin.name, {
202
+ ...existingOptions,
203
+ ...(plugin.options ?? {}),
204
+ });
205
+ };
206
+
207
+ for (const permissionRequirement of permissionsByName.values()) {
208
+ if (!isPermission(permissionRequirement.permission)) {
209
+ diagnostics.push({
210
+ severity: 'error',
211
+ requirementType: 'permission',
212
+ requirement: permissionRequirement.permission,
213
+ message: `Unknown permission '${permissionRequirement.permission}' cannot be resolved for Expo runtime generation.`,
214
+ });
215
+ continue;
216
+ }
217
+
218
+ const supportMetadata = permissionSupport[permissionRequirement.permission];
219
+ if (
220
+ supportMetadata.support === 'unsupported' ||
221
+ supportMetadata.support === 'notImplemented' ||
222
+ supportMetadata.support === 'limited'
223
+ ) {
224
+ diagnostics.push({
225
+ severity: supportMetadata.support === 'limited' ? 'warning' : 'error',
226
+ requirementType: 'permission',
227
+ requirement: permissionRequirement.permission,
228
+ message: `Expo runtime support for '${permissionRequirement.permission}' is '${supportMetadata.support}'.`,
229
+ support: supportMetadata.support,
230
+ });
231
+ continue;
232
+ }
233
+
234
+ if (supportMetadata.support === 'notRequired') {
235
+ continue;
236
+ }
237
+
238
+ addProvider('permissions', `permission:${permissionRequirement.permission}`);
239
+ supportMetadata.requiredPackages.forEach((packageName) => {
240
+ addDependency(packageName, `permission:${permissionRequirement.permission}`);
241
+ });
242
+
243
+ supportMetadata.configHints.forEach((configHint) => {
244
+ configHints.add(configHint);
245
+ const configHintMetadata = configHintRegistry[configHint];
246
+ if (!configHintMetadata) {
247
+ diagnostics.push({
248
+ severity: 'warning',
249
+ requirementType: 'configHint',
250
+ requirement: configHint,
251
+ message: `Config hint '${configHint}' is not translated into generated Expo config yet.`,
252
+ });
253
+ return;
254
+ }
255
+
256
+ configHintMetadata.androidPermissions?.forEach((permissionName) => {
257
+ androidPermissions.add(permissionName);
258
+ });
259
+ if (configHintMetadata.plugin) {
260
+ addPlugin(configHintMetadata.plugin);
261
+ }
262
+ });
263
+ }
264
+
265
+ for (const capabilityRequirement of capabilitiesByName.values()) {
266
+ const capabilityMetadata = capabilityRegistry[capabilityRequirement.capability];
267
+ if (!capabilityMetadata) {
268
+ diagnostics.push({
269
+ severity: 'warning',
270
+ requirementType: 'capability',
271
+ requirement: capabilityRequirement.capability,
272
+ message: `No Expo runtime metadata is registered for capability '${capabilityRequirement.capability}'.`,
273
+ });
274
+ continue;
275
+ }
276
+
277
+ capabilityMetadata.requiredPackages?.forEach((packageName) => {
278
+ addDependency(packageName, `capability:${capabilityRequirement.capability}`);
279
+ });
280
+ capabilityMetadata.providers?.forEach((provider) => {
281
+ addProvider(provider, `capability:${capabilityRequirement.capability}`);
282
+ });
283
+ capabilityMetadata.runtimeAdapters?.forEach((runtimeAdapter) => {
284
+ runtimeAdapters.add(runtimeAdapter);
285
+ });
286
+ capabilityMetadata.androidPermissions?.forEach((permissionName) => {
287
+ androidPermissions.add(permissionName);
288
+ });
289
+ capabilityMetadata.plugins?.forEach(addPlugin);
290
+ }
291
+
292
+ if (providers.size > 0 || runtimeAdapters.size > 0) {
293
+ addDependency(EXPO_RUNTIME_PACKAGE_NAME, 'runtime:expo');
294
+ }
295
+
296
+ const plugins = Array.from(pluginOptions.entries())
297
+ .map(([name, options]) => ({
298
+ name,
299
+ options: Object.keys(options).length > 0 ? options : undefined,
300
+ }))
301
+ .sort((left, right) => left.name.localeCompare(right.name));
302
+
303
+ return {
304
+ permissions: Array.from(permissionsByName.values()).sort(comparePermissionRequirements),
305
+ capabilities: Array.from(capabilitiesByName.values()).sort(compareCapabilityRequirements),
306
+ impliedPermissions: Array.from(impliedPermissionsByName.values()).sort(
307
+ comparePermissionRequirements,
308
+ ),
309
+ dependencies: Array.from(dependencies.values()).sort((left, right) =>
310
+ left.name.localeCompare(right.name),
311
+ ),
312
+ nativeConfig: {
313
+ androidPermissions: Array.from(androidPermissions).sort(),
314
+ configHints: Array.from(configHints).sort(),
315
+ plugins,
316
+ },
317
+ providers: Array.from(providers).sort(),
318
+ runtimeAdapters: Array.from(runtimeAdapters).sort(),
319
+ usesExpoRuntimeRegistry: runtimeAdapters.size > 0,
320
+ needsPermissionsProvider: providers.has('permissions'),
321
+ diagnostics,
322
+ };
323
+ }
324
+
325
+ function compareCapabilityRequirements(
326
+ left: ScreenCapabilityRequirement,
327
+ right: ScreenCapabilityRequirement,
328
+ ): number {
329
+ return left.capability.localeCompare(right.capability);
330
+ }
331
+
332
+ function comparePermissionRequirements(
333
+ left: ScreenPermissionRequirement,
334
+ right: ScreenPermissionRequirement,
335
+ ): number {
336
+ return left.permission.localeCompare(right.permission);
337
+ }