@ankhorage/expo-runtime 2.5.1 → 2.7.0

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 (51) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -2
  3. package/dist/ExpoBarcodeScannerAdapter.d.ts.map +1 -1
  4. package/dist/ExpoBarcodeScannerAdapter.js +4 -16
  5. package/dist/ExpoBarcodeScannerAdapter.js.map +1 -1
  6. package/dist/barcodeScanRuntime.d.ts +4 -1
  7. package/dist/barcodeScanRuntime.d.ts.map +1 -1
  8. package/dist/barcodeScanRuntime.js +15 -1
  9. package/dist/barcodeScanRuntime.js.map +1 -1
  10. package/dist/expoRuntimePlanningMetadata.d.ts +59 -0
  11. package/dist/expoRuntimePlanningMetadata.d.ts.map +1 -0
  12. package/dist/expoRuntimePlanningMetadata.js +133 -0
  13. package/dist/expoRuntimePlanningMetadata.js.map +1 -0
  14. package/dist/mediaPicker/documentPicker.d.ts +1 -2
  15. package/dist/mediaPicker/documentPicker.d.ts.map +1 -1
  16. package/dist/mediaPicker/documentPicker.js +1 -1
  17. package/dist/mediaPicker/documentPicker.js.map +1 -1
  18. package/dist/mediaPicker/photoLibraryPicker.d.ts +1 -2
  19. package/dist/mediaPicker/photoLibraryPicker.d.ts.map +1 -1
  20. package/dist/mediaPicker/photoLibraryPicker.js +1 -1
  21. package/dist/mediaPicker/photoLibraryPicker.js.map +1 -1
  22. package/dist/mediaPicker/readSelectedBytes.d.ts +1 -4
  23. package/dist/mediaPicker/readSelectedBytes.d.ts.map +1 -1
  24. package/dist/mediaPicker/readSelectedBytes.js +7 -1
  25. package/dist/mediaPicker/readSelectedBytes.js.map +1 -1
  26. package/dist/planning.d.ts +1 -1
  27. package/dist/planning.d.ts.map +1 -1
  28. package/dist/planning.js.map +1 -1
  29. package/dist/platform.d.ts +83 -0
  30. package/dist/platform.d.ts.map +1 -0
  31. package/dist/platform.js +81 -0
  32. package/dist/platform.js.map +1 -0
  33. package/dist/resolveExpoRuntimePlan.d.ts +14 -25
  34. package/dist/resolveExpoRuntimePlan.d.ts.map +1 -1
  35. package/dist/resolveExpoRuntimePlan.js +154 -186
  36. package/dist/resolveExpoRuntimePlan.js.map +1 -1
  37. package/package.json +45 -29
  38. package/src/ExpoBarcodeScannerAdapter.test.tsx +47 -4
  39. package/src/ExpoBarcodeScannerAdapter.tsx +10 -19
  40. package/src/barcodeScanRuntime.ts +27 -2
  41. package/src/expoRuntimePlanningMetadata.ts +190 -0
  42. package/src/generatedAppOutput.test.ts +29 -7
  43. package/src/mediaPicker/documentPicker.ts +1 -6
  44. package/src/mediaPicker/photoLibraryPicker.ts +1 -6
  45. package/src/mediaPicker/readSelectedBytes.ts +12 -3
  46. package/src/oauthBrowserRuntime.test.ts +1 -1
  47. package/src/planning.ts +2 -0
  48. package/src/platform.test.ts +34 -0
  49. package/src/platform.ts +88 -0
  50. package/src/resolveExpoRuntimePlan.test.ts +154 -101
  51. package/src/resolveExpoRuntimePlan.ts +226 -246
@@ -1,6 +1,5 @@
1
1
  import type {
2
2
  AnkhorageCapabilityName,
3
- AppManifest,
4
3
  ScreenCapabilityRequirement,
5
4
  ScreenPermissionRequirement,
6
5
  } from '@ankhorage/contracts';
@@ -12,26 +11,39 @@ import {
12
11
  type PermissionSupport,
13
12
  } from '@ankhorage/permissions/expo/manifest';
14
13
 
15
- interface ExpoRuntimeDependency {
16
- readonly name: string;
17
- readonly version: string;
18
- readonly reasons: readonly string[];
14
+ import {
15
+ EXPO_CAPABILITY_RUNTIME_REGISTRY,
16
+ EXPO_RUNTIME_PROVIDER_PACKAGES,
17
+ type ExpoRuntimeAdapterId,
18
+ type ExpoRuntimeCapabilityMetadata,
19
+ type ExpoRuntimeConfigPlugin,
20
+ type ExpoRuntimeDiagnostic,
21
+ type ExpoRuntimeProviderId,
22
+ findCapabilityMetadata,
23
+ findConfigHintMetadata,
24
+ GENERATED_RUNTIME_DEPENDENCY_VERSIONS,
25
+ unknownCapabilityDiagnostic,
26
+ unknownPermissionDiagnostic,
27
+ unsupportedPermissionDiagnostic,
28
+ } from './expoRuntimePlanningMetadata';
29
+
30
+ export type { ExpoRuntimeAdapterId, ExpoRuntimeProviderId };
31
+
32
+ export interface ExpoRuntimePlanningScreen {
33
+ readonly requires?: {
34
+ readonly capabilities?: readonly ScreenCapabilityRequirement[];
35
+ readonly permissions?: readonly ScreenPermissionRequirement[];
36
+ };
19
37
  }
20
38
 
21
- interface ExpoRuntimeConfigPlugin {
22
- readonly name: string;
23
- readonly options?: Readonly<Record<string, boolean | string>>;
39
+ export interface ExpoRuntimePlanningManifest {
40
+ readonly screens: Readonly<Record<string, ExpoRuntimePlanningScreen>>;
24
41
  }
25
42
 
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;
43
+ interface ExpoRuntimeDependency {
44
+ readonly name: string;
45
+ readonly version: string;
46
+ readonly reasons: readonly string[];
35
47
  }
36
48
 
37
49
  export interface ExpoRuntimePlan {
@@ -51,20 +63,6 @@ export interface ExpoRuntimePlan {
51
63
  readonly diagnostics: readonly ExpoRuntimeDiagnostic[];
52
64
  }
53
65
 
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
66
  interface ResolveExpoRuntimePlanOptions {
69
67
  readonly capabilityRegistry?: Readonly<
70
68
  Partial<Record<AnkhorageCapabilityName, ExpoRuntimeCapabilityMetadata>>
@@ -73,263 +71,245 @@ interface ResolveExpoRuntimePlanOptions {
73
71
  readonly permissionSupport?: Readonly<Record<Permission, ExpoPermissionMetadata>>;
74
72
  }
75
73
 
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>;
74
+ interface PlanningState {
75
+ readonly permissions: Map<string, ScreenPermissionRequirement>;
76
+ readonly impliedPermissions: Map<string, ScreenPermissionRequirement>;
77
+ readonly capabilities: Map<string, ScreenCapabilityRequirement>;
78
+ readonly diagnostics: ExpoRuntimeDiagnostic[];
79
+ readonly dependencies: Map<string, ExpoRuntimeDependency>;
80
+ readonly pluginOptions: Map<string, Record<string, boolean | string>>;
81
+ readonly configHints: Set<string>;
82
+ readonly androidPermissions: Set<string>;
83
+ readonly providers: Set<ExpoRuntimeProviderId>;
84
+ readonly runtimeAdapters: Set<ExpoRuntimeAdapterId>;
85
+ }
88
86
 
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>>;
87
+ interface PlanningContext {
88
+ readonly state: PlanningState;
89
+ readonly capabilityRegistry: Readonly<
90
+ Partial<Record<AnkhorageCapabilityName, ExpoRuntimeCapabilityMetadata>>
91
+ >;
92
+ readonly dependencyVersions: Readonly<Record<string, string>>;
93
+ readonly permissionSupport: Readonly<Record<Permission, ExpoPermissionMetadata>>;
94
+ }
100
95
 
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
- >;
96
+ const EXPO_RUNTIME_PACKAGE_NAME = '@ankhorage/expo-runtime';
120
97
 
121
98
  export function resolveExpoRuntimePlan(
122
- manifest: AppManifest,
99
+ manifest: ExpoRuntimePlanningManifest,
123
100
  options: ResolveExpoRuntimePlanOptions = {},
124
101
  ): 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
- );
102
+ const state = createPlanningState(manifest);
103
+ const context: PlanningContext = {
104
+ state,
105
+ capabilityRegistry: options.capabilityRegistry ?? EXPO_CAPABILITY_RUNTIME_REGISTRY,
106
+ dependencyVersions: options.dependencyVersions ?? GENERATED_RUNTIME_DEPENDENCY_VERSIONS,
107
+ permissionSupport: options.permissionSupport ?? EXPO_PERMISSION_SUPPORT,
108
+ };
109
+ addImpliedPermissions(context);
110
+ planPermissions(context);
111
+ planCapabilities(context);
112
+ if (state.providers.size > 0 || state.runtimeAdapters.size > 0) {
113
+ addDependency(context, EXPO_RUNTIME_PACKAGE_NAME, 'runtime:expo');
151
114
  }
115
+ return buildPlan(state);
116
+ }
152
117
 
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;
118
+ function createPlanningState(manifest: ExpoRuntimePlanningManifest): PlanningState {
119
+ const permissions = new Map<string, ScreenPermissionRequirement>();
120
+ const capabilities = new Map<string, ScreenCapabilityRequirement>();
121
+ for (const screen of Object.values(manifest.screens)) {
122
+ for (const requirement of screen.requires?.permissions ?? []) {
123
+ permissions.set(requirement.permission, requirement);
176
124
  }
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;
125
+ for (const requirement of screen.requires?.capabilities ?? []) {
126
+ capabilities.set(requirement.capability, requirement);
185
127
  }
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
- });
128
+ }
129
+ return {
130
+ permissions,
131
+ capabilities,
132
+ impliedPermissions: new Map(),
133
+ diagnostics: [],
134
+ dependencies: new Map(),
135
+ pluginOptions: new Map(),
136
+ configHints: new Set(),
137
+ androidPermissions: new Set(),
138
+ providers: new Set(),
139
+ runtimeAdapters: new Set(),
205
140
  };
141
+ }
206
142
 
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;
143
+ function addImpliedPermissions(context: PlanningContext): void {
144
+ const { state } = context;
145
+ for (const capability of state.capabilities.values()) {
146
+ const metadata = findCapabilityMetadata(context.capabilityRegistry, capability);
147
+ for (const permission of metadata?.impliedPermissions ?? []) {
148
+ if (!state.permissions.has(permission.permission)) {
149
+ state.permissions.set(permission.permission, permission);
150
+ state.impliedPermissions.set(permission.permission, permission);
151
+ }
236
152
  }
153
+ }
154
+ }
237
155
 
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
- }
156
+ function planPermissions(context: PlanningContext): void {
157
+ for (const requirement of context.state.permissions.values()) {
158
+ planPermission(context, requirement);
159
+ }
160
+ }
255
161
 
256
- configHintMetadata.androidPermissions?.forEach((permissionName) => {
257
- androidPermissions.add(permissionName);
258
- });
259
- if (configHintMetadata.plugin) {
260
- addPlugin(configHintMetadata.plugin);
261
- }
262
- });
162
+ function planPermission(context: PlanningContext, requirement: ScreenPermissionRequirement): void {
163
+ if (!isPermission(requirement.permission)) {
164
+ context.state.diagnostics.push(unknownPermissionDiagnostic(requirement.permission));
165
+ return;
166
+ }
167
+ const metadata = findPermissionSupport(context.permissionSupport, requirement.permission);
168
+ if (isUnsupported(metadata.support)) {
169
+ context.state.diagnostics.push(
170
+ unsupportedPermissionDiagnostic(requirement.permission, metadata.support),
171
+ );
172
+ return;
263
173
  }
174
+ if (metadata.support === 'notRequired') {
175
+ return;
176
+ }
177
+ const reason = `permission:${requirement.permission}`;
178
+ addProvider(context, 'permissions', reason);
179
+ metadata.requiredPackages.forEach((name) => addDependency(context, name, reason));
180
+ metadata.configHints.forEach((hint) => applyConfigHint(context.state, hint));
181
+ }
264
182
 
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
- });
183
+ function planCapabilities(context: PlanningContext): void {
184
+ for (const requirement of context.state.capabilities.values()) {
185
+ const metadata = findCapabilityMetadata(context.capabilityRegistry, requirement);
186
+ if (metadata === undefined) {
187
+ context.state.diagnostics.push(unknownCapabilityDiagnostic(requirement.capability));
274
188
  continue;
275
189
  }
190
+ const reason = `capability:${requirement.capability}`;
191
+ metadata.requiredPackages?.forEach((name) => addDependency(context, name, reason));
192
+ metadata.providers?.forEach((provider) => addProvider(context, provider, reason));
193
+ metadata.runtimeAdapters?.forEach((adapter) => context.state.runtimeAdapters.add(adapter));
194
+ metadata.androidPermissions?.forEach((permission) =>
195
+ context.state.androidPermissions.add(permission),
196
+ );
197
+ metadata.plugins?.forEach((plugin) => addPlugin(context.state, plugin));
198
+ }
199
+ }
276
200
 
277
- capabilityMetadata.requiredPackages?.forEach((packageName) => {
278
- addDependency(packageName, `capability:${capabilityRequirement.capability}`);
279
- });
280
- capabilityMetadata.providers?.forEach((provider) => {
281
- addProvider(provider, `capability:${capabilityRequirement.capability}`);
201
+ function applyConfigHint(state: PlanningState, configHint: string): void {
202
+ state.configHints.add(configHint);
203
+ const metadata = findConfigHintMetadata(configHint);
204
+ if (metadata === undefined) {
205
+ state.diagnostics.push({
206
+ severity: 'warning',
207
+ requirementType: 'configHint',
208
+ requirement: configHint,
209
+ message: `Config hint '${configHint}' is not translated into generated Expo config yet.`,
282
210
  });
283
- capabilityMetadata.runtimeAdapters?.forEach((runtimeAdapter) => {
284
- runtimeAdapters.add(runtimeAdapter);
285
- });
286
- capabilityMetadata.androidPermissions?.forEach((permissionName) => {
287
- androidPermissions.add(permissionName);
211
+ return;
212
+ }
213
+ metadata.androidPermissions?.forEach((permission) => state.androidPermissions.add(permission));
214
+ if (metadata.plugin !== undefined) {
215
+ addPlugin(state, metadata.plugin);
216
+ }
217
+ }
218
+
219
+ function addDependency(context: PlanningContext, name: string, reason: string): void {
220
+ const version = findDependencyVersion(context.dependencyVersions, name);
221
+ if (version === undefined) {
222
+ context.state.diagnostics.push({
223
+ severity: 'warning',
224
+ requirementType: 'package',
225
+ requirement: name,
226
+ message: `No generated-app dependency version is registered for '${name}'.`,
288
227
  });
289
- capabilityMetadata.plugins?.forEach(addPlugin);
228
+ return;
290
229
  }
230
+ const existing = context.state.dependencies.get(name);
231
+ context.state.dependencies.set(name, {
232
+ name,
233
+ version,
234
+ reasons: existing === undefined ? [reason] : Array.from(new Set([...existing.reasons, reason])),
235
+ });
236
+ }
291
237
 
292
- if (providers.size > 0 || runtimeAdapters.size > 0) {
293
- addDependency(EXPO_RUNTIME_PACKAGE_NAME, 'runtime:expo');
238
+ function addProvider(
239
+ context: PlanningContext,
240
+ provider: ExpoRuntimeProviderId,
241
+ reason: string,
242
+ ): void {
243
+ context.state.providers.add(provider);
244
+ const packageName = Object.entries(EXPO_RUNTIME_PROVIDER_PACKAGES).find(
245
+ ([name]) => name === String(provider),
246
+ )?.[1];
247
+ if (packageName === undefined) {
248
+ throw new Error(`Runtime package metadata is missing for provider '${provider}'.`);
294
249
  }
250
+ addDependency(context, packageName, reason);
251
+ }
295
252
 
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));
253
+ function addPlugin(state: PlanningState, plugin: ExpoRuntimeConfigPlugin): void {
254
+ const existingOptions = state.pluginOptions.get(plugin.name) ?? {};
255
+ state.pluginOptions.set(plugin.name, { ...existingOptions, ...(plugin.options ?? {}) });
256
+ }
302
257
 
258
+ function buildPlan(state: PlanningState): ExpoRuntimePlan {
259
+ const plugins = Array.from(state.pluginOptions, ([name, options]) => ({
260
+ name,
261
+ options: Object.keys(options).length > 0 ? options : undefined,
262
+ })).sort((left, right) => left.name.localeCompare(right.name));
303
263
  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) =>
264
+ permissions: Array.from(state.permissions.values()).sort(comparePermissions),
265
+ capabilities: Array.from(state.capabilities.values()).sort(compareCapabilities),
266
+ impliedPermissions: Array.from(state.impliedPermissions.values()).sort(comparePermissions),
267
+ dependencies: Array.from(state.dependencies.values()).sort((left, right) =>
310
268
  left.name.localeCompare(right.name),
311
269
  ),
312
270
  nativeConfig: {
313
- androidPermissions: Array.from(androidPermissions).sort(),
314
- configHints: Array.from(configHints).sort(),
271
+ androidPermissions: Array.from(state.androidPermissions).sort(),
272
+ configHints: Array.from(state.configHints).sort(),
315
273
  plugins,
316
274
  },
317
- providers: Array.from(providers).sort(),
318
- runtimeAdapters: Array.from(runtimeAdapters).sort(),
319
- usesExpoRuntimeRegistry: runtimeAdapters.size > 0,
320
- needsPermissionsProvider: providers.has('permissions'),
321
- diagnostics,
275
+ providers: Array.from(state.providers).sort(),
276
+ runtimeAdapters: Array.from(state.runtimeAdapters).sort(),
277
+ usesExpoRuntimeRegistry: state.runtimeAdapters.size > 0,
278
+ needsPermissionsProvider: state.providers.has('permissions'),
279
+ diagnostics: state.diagnostics,
322
280
  };
323
281
  }
324
282
 
325
- function compareCapabilityRequirements(
283
+ function findPermissionSupport(
284
+ registry: Readonly<Record<Permission, ExpoPermissionMetadata>>,
285
+ permission: Permission,
286
+ ): ExpoPermissionMetadata {
287
+ const metadata = Object.entries(registry).find(([name]) => name === String(permission))?.[1];
288
+ if (metadata === undefined) {
289
+ throw new Error(`Permission metadata is missing for '${permission}'.`);
290
+ }
291
+ return metadata;
292
+ }
293
+
294
+ function findDependencyVersion(
295
+ versions: Readonly<Record<string, string>>,
296
+ name: string,
297
+ ): string | undefined {
298
+ return Object.entries(versions).find(([candidate]) => candidate === name)?.[1];
299
+ }
300
+
301
+ function isUnsupported(support: PermissionSupport): boolean {
302
+ return support === 'unsupported' || support === 'notImplemented' || support === 'limited';
303
+ }
304
+
305
+ function compareCapabilities(
326
306
  left: ScreenCapabilityRequirement,
327
307
  right: ScreenCapabilityRequirement,
328
308
  ): number {
329
309
  return left.capability.localeCompare(right.capability);
330
310
  }
331
311
 
332
- function comparePermissionRequirements(
312
+ function comparePermissions(
333
313
  left: ScreenPermissionRequirement,
334
314
  right: ScreenPermissionRequirement,
335
315
  ): number {