@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.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -2
- package/dist/ExpoBarcodeScannerAdapter.d.ts.map +1 -1
- package/dist/ExpoBarcodeScannerAdapter.js +4 -16
- package/dist/ExpoBarcodeScannerAdapter.js.map +1 -1
- package/dist/barcodeScanRuntime.d.ts +4 -1
- package/dist/barcodeScanRuntime.d.ts.map +1 -1
- package/dist/barcodeScanRuntime.js +15 -1
- package/dist/barcodeScanRuntime.js.map +1 -1
- package/dist/expoRuntimePlanningMetadata.d.ts +59 -0
- package/dist/expoRuntimePlanningMetadata.d.ts.map +1 -0
- package/dist/expoRuntimePlanningMetadata.js +133 -0
- package/dist/expoRuntimePlanningMetadata.js.map +1 -0
- package/dist/mediaPicker/documentPicker.d.ts +1 -2
- package/dist/mediaPicker/documentPicker.d.ts.map +1 -1
- package/dist/mediaPicker/documentPicker.js +1 -1
- package/dist/mediaPicker/documentPicker.js.map +1 -1
- package/dist/mediaPicker/photoLibraryPicker.d.ts +1 -2
- package/dist/mediaPicker/photoLibraryPicker.d.ts.map +1 -1
- package/dist/mediaPicker/photoLibraryPicker.js +1 -1
- package/dist/mediaPicker/photoLibraryPicker.js.map +1 -1
- package/dist/mediaPicker/readSelectedBytes.d.ts +1 -4
- package/dist/mediaPicker/readSelectedBytes.d.ts.map +1 -1
- package/dist/mediaPicker/readSelectedBytes.js +7 -1
- package/dist/mediaPicker/readSelectedBytes.js.map +1 -1
- package/dist/planning.d.ts +1 -1
- package/dist/planning.d.ts.map +1 -1
- package/dist/planning.js.map +1 -1
- package/dist/platform.d.ts +83 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +81 -0
- package/dist/platform.js.map +1 -0
- package/dist/resolveExpoRuntimePlan.d.ts +14 -25
- package/dist/resolveExpoRuntimePlan.d.ts.map +1 -1
- package/dist/resolveExpoRuntimePlan.js +154 -186
- package/dist/resolveExpoRuntimePlan.js.map +1 -1
- package/package.json +45 -29
- package/src/ExpoBarcodeScannerAdapter.test.tsx +47 -4
- package/src/ExpoBarcodeScannerAdapter.tsx +10 -19
- package/src/barcodeScanRuntime.ts +27 -2
- package/src/expoRuntimePlanningMetadata.ts +190 -0
- package/src/generatedAppOutput.test.ts +29 -7
- package/src/mediaPicker/documentPicker.ts +1 -6
- package/src/mediaPicker/photoLibraryPicker.ts +1 -6
- package/src/mediaPicker/readSelectedBytes.ts +12 -3
- package/src/oauthBrowserRuntime.test.ts +1 -1
- package/src/planning.ts +2 -0
- package/src/platform.test.ts +34 -0
- package/src/platform.ts +88 -0
- package/src/resolveExpoRuntimePlan.test.ts +154 -101
- package/src/resolveExpoRuntimePlan.ts +226 -246
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
resolveExpoRuntimeGeneratedAppOutput,
|
|
7
7
|
resolveExpoRuntimeNativeOutput,
|
|
8
8
|
} from './generatedAppOutput';
|
|
9
|
+
import { EXPO_PLATFORM } from './platform';
|
|
9
10
|
import type { ExpoRuntimePlan } from './resolveExpoRuntimePlan';
|
|
10
11
|
|
|
11
12
|
const EMPTY_PLAN: ExpoRuntimePlan = {
|
|
@@ -32,7 +33,7 @@ const CAMERA_PLAN: ExpoRuntimePlan = {
|
|
|
32
33
|
dependencies: [
|
|
33
34
|
{
|
|
34
35
|
name: 'expo-camera',
|
|
35
|
-
version:
|
|
36
|
+
version: EXPO_PLATFORM.packages.camera.version,
|
|
36
37
|
reasons: ['capability:barcodeScanner'],
|
|
37
38
|
},
|
|
38
39
|
{
|
|
@@ -42,13 +43,16 @@ const CAMERA_PLAN: ExpoRuntimePlan = {
|
|
|
42
43
|
},
|
|
43
44
|
],
|
|
44
45
|
nativeConfig: {
|
|
45
|
-
androidPermissions: [
|
|
46
|
+
androidPermissions: [],
|
|
46
47
|
configHints: ['cameraPermission'],
|
|
47
48
|
plugins: [
|
|
48
49
|
{
|
|
49
50
|
name: 'expo-camera',
|
|
50
51
|
options: {
|
|
51
52
|
cameraPermission: 'Allow camera access to scan barcodes.',
|
|
53
|
+
barcodeScannerEnabled: true,
|
|
54
|
+
microphonePermission: false,
|
|
55
|
+
recordAudioAndroid: false,
|
|
52
56
|
},
|
|
53
57
|
},
|
|
54
58
|
{
|
|
@@ -62,10 +66,18 @@ const CAMERA_PLAN: ExpoRuntimePlan = {
|
|
|
62
66
|
needsPermissionsProvider: true,
|
|
63
67
|
};
|
|
64
68
|
|
|
65
|
-
describe('
|
|
69
|
+
describe('generated App config output', () => {
|
|
66
70
|
test('keeps Expo config plugins as a distinct external ecosystem concept', () => {
|
|
67
71
|
expect(resolveExpoRuntimeConfigPluginOutput(CAMERA_PLAN)).toEqual([
|
|
68
|
-
[
|
|
72
|
+
[
|
|
73
|
+
'expo-camera',
|
|
74
|
+
{
|
|
75
|
+
barcodeScannerEnabled: true,
|
|
76
|
+
cameraPermission: 'Allow camera access to scan barcodes.',
|
|
77
|
+
microphonePermission: false,
|
|
78
|
+
recordAudioAndroid: false,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
69
81
|
'expo-something',
|
|
70
82
|
]);
|
|
71
83
|
});
|
|
@@ -78,11 +90,13 @@ describe('generatedAppOutput', () => {
|
|
|
78
90
|
configPlugins: [],
|
|
79
91
|
});
|
|
80
92
|
});
|
|
93
|
+
});
|
|
81
94
|
|
|
95
|
+
describe('generated App dependency output', () => {
|
|
82
96
|
test('maps runtime dependencies into a package dependency map', () => {
|
|
83
97
|
expect(resolveExpoRuntimeDependencyMap(CAMERA_PLAN)).toEqual({
|
|
84
98
|
'@ankhorage/expo-runtime': '^0.0.7',
|
|
85
|
-
'expo-camera':
|
|
99
|
+
'expo-camera': EXPO_PLATFORM.packages.camera.version,
|
|
86
100
|
});
|
|
87
101
|
});
|
|
88
102
|
|
|
@@ -91,18 +105,23 @@ describe('generatedAppOutput', () => {
|
|
|
91
105
|
[
|
|
92
106
|
'expo-camera',
|
|
93
107
|
{
|
|
108
|
+
barcodeScannerEnabled: true,
|
|
94
109
|
cameraPermission: 'Allow camera access to scan barcodes.',
|
|
110
|
+
microphonePermission: false,
|
|
111
|
+
recordAudioAndroid: false,
|
|
95
112
|
},
|
|
96
113
|
],
|
|
97
114
|
'expo-something',
|
|
98
115
|
]);
|
|
99
116
|
});
|
|
117
|
+
});
|
|
100
118
|
|
|
119
|
+
describe('combined generated App output', () => {
|
|
101
120
|
test('combines dependency, native and layout output', () => {
|
|
102
121
|
expect(resolveExpoRuntimeGeneratedAppOutput(CAMERA_PLAN)).toEqual({
|
|
103
122
|
dependencies: {
|
|
104
123
|
'@ankhorage/expo-runtime': '^0.0.7',
|
|
105
|
-
'expo-camera':
|
|
124
|
+
'expo-camera': EXPO_PLATFORM.packages.camera.version,
|
|
106
125
|
},
|
|
107
126
|
layoutIntegration: {
|
|
108
127
|
imports: ["import { ExpoRuntimeProviders } from '@ankhorage/expo-runtime';"],
|
|
@@ -111,12 +130,15 @@ describe('generatedAppOutput', () => {
|
|
|
111
130
|
providerEnd: ['</ExpoRuntimeProviders>'],
|
|
112
131
|
},
|
|
113
132
|
native: {
|
|
114
|
-
androidPermissions: [
|
|
133
|
+
androidPermissions: [],
|
|
115
134
|
configPlugins: [
|
|
116
135
|
[
|
|
117
136
|
'expo-camera',
|
|
118
137
|
{
|
|
138
|
+
barcodeScannerEnabled: true,
|
|
119
139
|
cameraPermission: 'Allow camera access to scan barcodes.',
|
|
140
|
+
microphonePermission: false,
|
|
141
|
+
recordAudioAndroid: false,
|
|
120
142
|
},
|
|
121
143
|
],
|
|
122
144
|
'expo-something',
|
|
@@ -5,18 +5,13 @@ import {
|
|
|
5
5
|
isRequestedMediaKind,
|
|
6
6
|
resolveDocumentPickerMimeTypes,
|
|
7
7
|
} from './mediaKinds';
|
|
8
|
-
import {
|
|
9
|
-
type ArrayBufferReadable,
|
|
10
|
-
readSelectedMediaBytes,
|
|
11
|
-
type SelectedMediaByteSource,
|
|
12
|
-
} from './readSelectedBytes';
|
|
8
|
+
import { readSelectedMediaBytes, type SelectedMediaByteSource } from './readSelectedBytes';
|
|
13
9
|
import type { ExpoMediaPickerInput, ExpoMediaPickerResult } from './types';
|
|
14
10
|
|
|
15
11
|
interface DocumentPickerAssetLike extends SelectedMediaByteSource {
|
|
16
12
|
readonly name: string;
|
|
17
13
|
readonly mimeType?: string;
|
|
18
14
|
readonly size?: number;
|
|
19
|
-
readonly file?: ArrayBufferReadable;
|
|
20
15
|
}
|
|
21
16
|
|
|
22
17
|
interface DocumentPickerResultLike {
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import type { MediaAssetKind } from '@ankhorage/contracts';
|
|
2
2
|
|
|
3
3
|
import { isRequestedMediaKind } from './mediaKinds';
|
|
4
|
-
import {
|
|
5
|
-
type ArrayBufferReadable,
|
|
6
|
-
readSelectedMediaBytes,
|
|
7
|
-
type SelectedMediaByteSource,
|
|
8
|
-
} from './readSelectedBytes';
|
|
4
|
+
import { readSelectedMediaBytes, type SelectedMediaByteSource } from './readSelectedBytes';
|
|
9
5
|
import type { ExpoMediaPickerInput, ExpoMediaPickerResult } from './types';
|
|
10
6
|
|
|
11
7
|
export type ExpoPhotoLibraryMediaType = 'images' | 'videos';
|
|
@@ -18,7 +14,6 @@ interface PhotoLibraryAssetLike extends SelectedMediaByteSource {
|
|
|
18
14
|
readonly height: number;
|
|
19
15
|
readonly duration?: number | null;
|
|
20
16
|
readonly type?: 'image' | 'video' | 'livePhoto' | 'pairedVideo' | null;
|
|
21
|
-
readonly file?: ArrayBufferReadable;
|
|
22
17
|
}
|
|
23
18
|
|
|
24
19
|
interface PhotoLibraryResultLike {
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
interface ArrayBufferReadable {
|
|
2
2
|
arrayBuffer(): Promise<ArrayBuffer>;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
export interface SelectedMediaByteSource {
|
|
6
6
|
readonly uri: string;
|
|
7
|
-
readonly file?:
|
|
7
|
+
readonly file?: unknown;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export async function readSelectedMediaBytes(
|
|
11
11
|
source: SelectedMediaByteSource,
|
|
12
12
|
): Promise<Uint8Array | null> {
|
|
13
13
|
try {
|
|
14
|
-
if (source.file) {
|
|
14
|
+
if (isArrayBufferReadable(source.file)) {
|
|
15
15
|
return new Uint8Array(await source.file.arrayBuffer());
|
|
16
16
|
}
|
|
17
17
|
const { File } = await import('expo-file-system');
|
|
@@ -20,3 +20,12 @@ export async function readSelectedMediaBytes(
|
|
|
20
20
|
return null;
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
function isArrayBufferReadable(value: unknown): value is ArrayBufferReadable {
|
|
25
|
+
return (
|
|
26
|
+
typeof value === 'object' &&
|
|
27
|
+
value !== null &&
|
|
28
|
+
'arrayBuffer' in value &&
|
|
29
|
+
typeof value.arrayBuffer === 'function'
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -3,7 +3,7 @@ import { describe, expect, test } from 'bun:test';
|
|
|
3
3
|
import { resolveExpoOAuthBrowserRuntimeReadinessFromEnvironment } from './oauthBrowserRuntimeCore';
|
|
4
4
|
|
|
5
5
|
describe('Expo OAuth browser runtime readiness', () => {
|
|
6
|
-
test('allows
|
|
6
|
+
test('allows a custom native build with an embedded Expo config', () => {
|
|
7
7
|
expect(
|
|
8
8
|
resolveExpoOAuthBrowserRuntimeReadinessFromEnvironment({
|
|
9
9
|
appOwnership: null,
|
package/src/planning.ts
CHANGED
|
@@ -21,6 +21,8 @@ export {
|
|
|
21
21
|
export type {
|
|
22
22
|
ExpoRuntimeAdapterId,
|
|
23
23
|
ExpoRuntimePlan,
|
|
24
|
+
ExpoRuntimePlanningManifest,
|
|
25
|
+
ExpoRuntimePlanningScreen,
|
|
24
26
|
ExpoRuntimeProviderId,
|
|
25
27
|
} from './resolveExpoRuntimePlan';
|
|
26
28
|
export { resolveExpoRuntimePlan } from './resolveExpoRuntimePlan';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { EXPO_PLATFORM } from './platform';
|
|
4
|
+
|
|
5
|
+
describe('EXPO_PLATFORM', () => {
|
|
6
|
+
test('represents the supported Expo 57 native platform without a tvOS claim', () => {
|
|
7
|
+
expect(EXPO_PLATFORM.sdk).toBe(57);
|
|
8
|
+
expect(EXPO_PLATFORM.architecture).toBe('new-architecture-only');
|
|
9
|
+
expect(EXPO_PLATFORM.platforms).toEqual({
|
|
10
|
+
android: {
|
|
11
|
+
minimumVersion: 7,
|
|
12
|
+
minimumApiLevel: 24,
|
|
13
|
+
compileSdk: 36,
|
|
14
|
+
targetSdk: 36,
|
|
15
|
+
edgeToEdge: true,
|
|
16
|
+
},
|
|
17
|
+
ios: {
|
|
18
|
+
minimumVersion: '16.4',
|
|
19
|
+
minimumXcodeVersion: '26.4',
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
expect('tvos' in EXPO_PLATFORM.platforms).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('keeps the Hermes fix, React Native and Node policy in one contract', () => {
|
|
26
|
+
expect(EXPO_PLATFORM.runtime.expo).toEqual({ name: 'expo', version: '~57.0.15' });
|
|
27
|
+
expect(EXPO_PLATFORM.runtime.reactNative).toEqual({
|
|
28
|
+
name: 'react-native',
|
|
29
|
+
version: '0.86.2',
|
|
30
|
+
});
|
|
31
|
+
expect(EXPO_PLATFORM.tooling.node).toEqual({ lts: 24, version: '24.x' });
|
|
32
|
+
expect(EXPO_PLATFORM.animation.worklets.version).toBe('0.10.1');
|
|
33
|
+
});
|
|
34
|
+
});
|
package/src/platform.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export interface ExpoPlatformPackage {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly version: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function definePackage(name: string, version: string): ExpoPlatformPackage {
|
|
7
|
+
return { name, version };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The canonical Ankhorage projection of the supported Expo platform.
|
|
12
|
+
*
|
|
13
|
+
* Package versions come from Expo SDK 57's bundled native module map. Keep this
|
|
14
|
+
* structure as the only hand-maintained Expo compatibility authority in this
|
|
15
|
+
* repository; generated manifests and runtime planning derive from it.
|
|
16
|
+
*/
|
|
17
|
+
export const EXPO_PLATFORM = {
|
|
18
|
+
sdk: 57,
|
|
19
|
+
architecture: 'new-architecture-only',
|
|
20
|
+
runtime: {
|
|
21
|
+
expo: definePackage('expo', '~57.0.15'),
|
|
22
|
+
react: definePackage('react', '19.2.3'),
|
|
23
|
+
reactDom: definePackage('react-dom', '19.2.3'),
|
|
24
|
+
reactNative: definePackage('react-native', '0.86.2'),
|
|
25
|
+
reactNativeWeb: definePackage('react-native-web', '~0.21.0'),
|
|
26
|
+
},
|
|
27
|
+
tooling: {
|
|
28
|
+
node: {
|
|
29
|
+
lts: 24,
|
|
30
|
+
version: '24.x',
|
|
31
|
+
},
|
|
32
|
+
nodeTypes: definePackage('@types/node', '^24.13.3'),
|
|
33
|
+
typescript: definePackage('typescript', '~6.0.3'),
|
|
34
|
+
expoDoctor: definePackage('expo-doctor', '1.20.2'),
|
|
35
|
+
},
|
|
36
|
+
navigation: {
|
|
37
|
+
expoRouter: definePackage('expo-router', '~57.0.15'),
|
|
38
|
+
screens: definePackage('react-native-screens', '~4.26.0'),
|
|
39
|
+
safeArea: definePackage('react-native-safe-area-context', '~5.7.0'),
|
|
40
|
+
},
|
|
41
|
+
animation: {
|
|
42
|
+
reanimated: definePackage('react-native-reanimated', '4.5.1'),
|
|
43
|
+
worklets: definePackage('react-native-worklets', '0.10.1'),
|
|
44
|
+
gestureHandler: definePackage('react-native-gesture-handler', '~2.32.0'),
|
|
45
|
+
},
|
|
46
|
+
ui: {
|
|
47
|
+
svg: definePackage('react-native-svg', '15.15.4'),
|
|
48
|
+
},
|
|
49
|
+
packages: {
|
|
50
|
+
metroRuntime: definePackage('@expo/metro-runtime', '~57.0.12'),
|
|
51
|
+
asset: definePackage('expo-asset', '~57.0.13'),
|
|
52
|
+
audio: definePackage('expo-audio', '~57.0.4'),
|
|
53
|
+
authSession: definePackage('expo-auth-session', '~57.0.8'),
|
|
54
|
+
camera: definePackage('expo-camera', '~57.0.4'),
|
|
55
|
+
constants: definePackage('expo-constants', '~57.0.13'),
|
|
56
|
+
devClient: definePackage('expo-dev-client', '~57.0.14'),
|
|
57
|
+
documentPicker: definePackage('expo-document-picker', '~57.0.1'),
|
|
58
|
+
fileSystem: definePackage('expo-file-system', '~57.0.5'),
|
|
59
|
+
font: definePackage('expo-font', '~57.0.1'),
|
|
60
|
+
imagePicker: definePackage('expo-image-picker', '~57.0.12'),
|
|
61
|
+
linearGradient: definePackage('expo-linear-gradient', '~57.0.1'),
|
|
62
|
+
linking: definePackage('expo-linking', '~57.0.7'),
|
|
63
|
+
localization: definePackage('expo-localization', '~57.0.1'),
|
|
64
|
+
location: definePackage('expo-location', '~57.0.12'),
|
|
65
|
+
mediaLibrary: definePackage('expo-media-library', '~57.0.4'),
|
|
66
|
+
notifications: definePackage('expo-notifications', '~57.0.13'),
|
|
67
|
+
secureStore: definePackage('expo-secure-store', '~57.0.1'),
|
|
68
|
+
splashScreen: definePackage('expo-splash-screen', '~57.0.7'),
|
|
69
|
+
statusBar: definePackage('expo-status-bar', '~57.0.1'),
|
|
70
|
+
updates: definePackage('expo-updates', '~57.0.16'),
|
|
71
|
+
webBrowser: definePackage('expo-web-browser', '~57.0.2'),
|
|
72
|
+
},
|
|
73
|
+
platforms: {
|
|
74
|
+
android: {
|
|
75
|
+
minimumVersion: 7,
|
|
76
|
+
minimumApiLevel: 24,
|
|
77
|
+
compileSdk: 36,
|
|
78
|
+
targetSdk: 36,
|
|
79
|
+
edgeToEdge: true,
|
|
80
|
+
},
|
|
81
|
+
ios: {
|
|
82
|
+
minimumVersion: '16.4',
|
|
83
|
+
minimumXcodeVersion: '26.4',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
} as const;
|
|
87
|
+
|
|
88
|
+
export type ExpoPlatform = typeof EXPO_PLATFORM;
|
|
@@ -1,16 +1,26 @@
|
|
|
1
|
-
import type { AppManifest } from '@ankhorage/contracts';
|
|
2
1
|
import { Permission } from '@ankhorage/permissions';
|
|
3
2
|
import { EXPO_PERMISSION_SUPPORT } from '@ankhorage/permissions/expo/manifest';
|
|
4
3
|
import { describe, expect, test } from 'bun:test';
|
|
5
4
|
|
|
6
|
-
import {
|
|
5
|
+
import { EXPO_PLATFORM } from './platform';
|
|
6
|
+
import { type ExpoRuntimePlanningManifest, resolveExpoRuntimePlan } from './resolveExpoRuntimePlan';
|
|
7
|
+
|
|
8
|
+
describe('resolveExpoRuntimePlan permissions', () => {
|
|
9
|
+
test('accepts a minimal screens-only planning manifest', () => {
|
|
10
|
+
const manifest: ExpoRuntimePlanningManifest = {
|
|
11
|
+
screens: {
|
|
12
|
+
home: {
|
|
13
|
+
requires: { permissions: [{ permission: 'camera' }] },
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
expect(resolveExpoRuntimePlan(manifest).permissions).toEqual([{ permission: 'camera' }]);
|
|
19
|
+
});
|
|
7
20
|
|
|
8
|
-
describe('resolveExpoRuntimePlan', () => {
|
|
9
21
|
test('resolves camera permission through Expo permission metadata', () => {
|
|
10
22
|
const plan = resolveExpoRuntimePlan(
|
|
11
|
-
withFirstScreenRequirements({
|
|
12
|
-
permissions: [{ permission: 'camera' }],
|
|
13
|
-
}),
|
|
23
|
+
withFirstScreenRequirements({ permissions: [{ permission: 'camera' }] }),
|
|
14
24
|
);
|
|
15
25
|
|
|
16
26
|
expect(plan.dependencies.map((dependency) => dependency.name)).toEqual([
|
|
@@ -21,22 +31,28 @@ describe('resolveExpoRuntimePlan', () => {
|
|
|
21
31
|
expect(plan.providers).toEqual(['permissions']);
|
|
22
32
|
expect(plan.needsPermissionsProvider).toBe(true);
|
|
23
33
|
expect(plan.nativeConfig.configHints).toEqual(['cameraPermission']);
|
|
24
|
-
expect(plan.nativeConfig.androidPermissions).toEqual([
|
|
34
|
+
expect(plan.nativeConfig.androidPermissions).toEqual([]);
|
|
25
35
|
expect(plan.nativeConfig.plugins).toEqual([
|
|
26
36
|
{
|
|
27
37
|
name: 'expo-camera',
|
|
28
38
|
options: {
|
|
39
|
+
barcodeScannerEnabled: false,
|
|
29
40
|
cameraPermission: 'Allow camera access.',
|
|
41
|
+
microphonePermission: false,
|
|
42
|
+
recordAudioAndroid: false,
|
|
30
43
|
},
|
|
31
44
|
},
|
|
32
45
|
]);
|
|
46
|
+
expect(plan.dependencies.find(({ name }) => name === 'expo-camera')?.version).toBe(
|
|
47
|
+
EXPO_PLATFORM.packages.camera.version,
|
|
48
|
+
);
|
|
33
49
|
});
|
|
50
|
+
});
|
|
34
51
|
|
|
52
|
+
describe('resolveExpoRuntimePlan barcode capability', () => {
|
|
35
53
|
test('resolves barcodeScanner capability with implied camera permission and adapter wiring', () => {
|
|
36
54
|
const plan = resolveExpoRuntimePlan(
|
|
37
|
-
withFirstScreenRequirements({
|
|
38
|
-
capabilities: [{ capability: 'barcodeScanner' }],
|
|
39
|
-
}),
|
|
55
|
+
withFirstScreenRequirements({ capabilities: [{ capability: 'barcodeScanner' }] }),
|
|
40
56
|
);
|
|
41
57
|
|
|
42
58
|
expect(plan.impliedPermissions).toEqual([{ permission: 'camera' }]);
|
|
@@ -51,12 +67,78 @@ describe('resolveExpoRuntimePlan', () => {
|
|
|
51
67
|
{
|
|
52
68
|
name: 'expo-camera',
|
|
53
69
|
options: {
|
|
70
|
+
barcodeScannerEnabled: true,
|
|
54
71
|
cameraPermission: 'Allow camera access to scan barcodes.',
|
|
72
|
+
microphonePermission: false,
|
|
73
|
+
recordAudioAndroid: false,
|
|
55
74
|
},
|
|
56
75
|
},
|
|
57
76
|
]);
|
|
58
77
|
});
|
|
59
78
|
|
|
79
|
+
test('lets barcodeScanner capability override camera-only scanner disabling', () => {
|
|
80
|
+
const plan = resolveExpoRuntimePlan(
|
|
81
|
+
withFirstScreenRequirements({
|
|
82
|
+
capabilities: [{ capability: 'barcodeScanner' }],
|
|
83
|
+
permissions: [{ permission: 'camera' }],
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(plan.nativeConfig.plugins).toContainEqual({
|
|
88
|
+
name: 'expo-camera',
|
|
89
|
+
options: {
|
|
90
|
+
barcodeScannerEnabled: true,
|
|
91
|
+
cameraPermission: 'Allow camera access to scan barcodes.',
|
|
92
|
+
microphonePermission: false,
|
|
93
|
+
recordAudioAndroid: false,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('resolveExpoRuntimePlan Expo 57 plugin coverage', () => {
|
|
100
|
+
test('keeps microphone-only native configuration free of background audio behavior', () => {
|
|
101
|
+
const plan = resolveExpoRuntimePlan(
|
|
102
|
+
withFirstScreenRequirements({ permissions: [{ permission: 'microphone' }] }),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(plan.nativeConfig.plugins).toEqual([
|
|
106
|
+
{
|
|
107
|
+
name: 'expo-audio',
|
|
108
|
+
options: {
|
|
109
|
+
enableBackgroundPlayback: false,
|
|
110
|
+
enableBackgroundRecording: false,
|
|
111
|
+
microphonePermission: 'Allow microphone access.',
|
|
112
|
+
recordAudioAndroid: true,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('translates every supported permission into current Expo 57 packages and plugins', () => {
|
|
119
|
+
const plan = resolveExpoRuntimePlan(
|
|
120
|
+
withFirstScreenRequirements({
|
|
121
|
+
permissions: [
|
|
122
|
+
{ permission: 'microphone' },
|
|
123
|
+
{ permission: 'mediaLibrary' },
|
|
124
|
+
{ permission: 'mediaLibraryWrite' },
|
|
125
|
+
{ permission: 'locationForeground' },
|
|
126
|
+
{ permission: 'locationBackground' },
|
|
127
|
+
{ permission: 'notifications' },
|
|
128
|
+
],
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
expect(plan.diagnostics).toEqual([]);
|
|
133
|
+
expect(plan.dependencies.map(({ name, version }) => ({ name, version }))).toEqual(
|
|
134
|
+
SUPPORTED_PERMISSION_DEPENDENCIES,
|
|
135
|
+
);
|
|
136
|
+
expect(plan.nativeConfig.androidPermissions).toEqual([]);
|
|
137
|
+
expect(plan.nativeConfig.plugins).toEqual(SUPPORTED_PERMISSION_PLUGINS);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('resolveExpoRuntimePlan diagnostics and deduplication', () => {
|
|
60
142
|
test('dedupes repeated permission and capability requirements', () => {
|
|
61
143
|
const plan = resolveExpoRuntimePlan(
|
|
62
144
|
withAllScreenRequirements({
|
|
@@ -76,9 +158,7 @@ describe('resolveExpoRuntimePlan', () => {
|
|
|
76
158
|
|
|
77
159
|
test('surfaces unsupported permission support explicitly', () => {
|
|
78
160
|
const plan = resolveExpoRuntimePlan(
|
|
79
|
-
withFirstScreenRequirements({
|
|
80
|
-
permissions: [{ permission: 'camera' }],
|
|
81
|
-
}),
|
|
161
|
+
withFirstScreenRequirements({ permissions: [{ permission: 'camera' }] }),
|
|
82
162
|
{
|
|
83
163
|
permissionSupport: {
|
|
84
164
|
...EXPO_PERMISSION_SUPPORT,
|
|
@@ -100,12 +180,12 @@ describe('resolveExpoRuntimePlan', () => {
|
|
|
100
180
|
expect(plan.dependencies).toEqual([]);
|
|
101
181
|
expect(plan.providers).toEqual([]);
|
|
102
182
|
});
|
|
183
|
+
});
|
|
103
184
|
|
|
185
|
+
describe('resolveExpoRuntimePlan platform-neutral permissions', () => {
|
|
104
186
|
test('does not add runtime packages or providers for clipboard permissions', () => {
|
|
105
187
|
const plan = resolveExpoRuntimePlan(
|
|
106
|
-
withFirstScreenRequirements({
|
|
107
|
-
permissions: [{ permission: 'clipboard' }],
|
|
108
|
-
}),
|
|
188
|
+
withFirstScreenRequirements({ permissions: [{ permission: 'clipboard' }] }),
|
|
109
189
|
);
|
|
110
190
|
|
|
111
191
|
expect(plan.dependencies).toEqual([]);
|
|
@@ -114,99 +194,72 @@ describe('resolveExpoRuntimePlan', () => {
|
|
|
114
194
|
});
|
|
115
195
|
});
|
|
116
196
|
|
|
117
|
-
|
|
197
|
+
const SUPPORTED_PERMISSION_DEPENDENCIES = [
|
|
198
|
+
{ name: '@ankhorage/expo-runtime', version: '^2.7.0' },
|
|
199
|
+
{ name: '@ankhorage/permissions', version: '^0.2.2' },
|
|
200
|
+
{ name: 'expo-audio', version: EXPO_PLATFORM.packages.audio.version },
|
|
201
|
+
{ name: 'expo-location', version: EXPO_PLATFORM.packages.location.version },
|
|
202
|
+
{ name: 'expo-media-library', version: EXPO_PLATFORM.packages.mediaLibrary.version },
|
|
203
|
+
{ name: 'expo-notifications', version: EXPO_PLATFORM.packages.notifications.version },
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
const SUPPORTED_PERMISSION_PLUGINS = [
|
|
207
|
+
{
|
|
208
|
+
name: 'expo-audio',
|
|
209
|
+
options: {
|
|
210
|
+
enableBackgroundPlayback: false,
|
|
211
|
+
enableBackgroundRecording: false,
|
|
212
|
+
microphonePermission: 'Allow microphone access.',
|
|
213
|
+
recordAudioAndroid: true,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'expo-location',
|
|
218
|
+
options: {
|
|
219
|
+
isAndroidBackgroundLocationEnabled: true,
|
|
220
|
+
isAndroidForegroundServiceEnabled: true,
|
|
221
|
+
isIosBackgroundLocationEnabled: true,
|
|
222
|
+
locationAlwaysAndWhenInUsePermission: 'Allow location access.',
|
|
223
|
+
locationWhenInUsePermission: 'Allow location access while using the app.',
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'expo-media-library',
|
|
228
|
+
options: {
|
|
229
|
+
photosPermission: 'Allow photo library access.',
|
|
230
|
+
savePhotosPermission: 'Allow saving to the photo library.',
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
{ name: 'expo-notifications' },
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
interface TestScreenRequirements {
|
|
237
|
+
readonly capabilities?: NonNullable<
|
|
238
|
+
ExpoRuntimePlanningManifest['screens'][string]['requires']
|
|
239
|
+
>['capabilities'];
|
|
240
|
+
readonly permissions?: NonNullable<
|
|
241
|
+
ExpoRuntimePlanningManifest['screens'][string]['requires']
|
|
242
|
+
>['permissions'];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function withFirstScreenRequirements(
|
|
246
|
+
requirements: TestScreenRequirements,
|
|
247
|
+
): ExpoRuntimePlanningManifest {
|
|
118
248
|
return {
|
|
119
|
-
...BASE_MANIFEST,
|
|
120
249
|
screens: {
|
|
121
|
-
home: {
|
|
122
|
-
|
|
123
|
-
requires: requirements,
|
|
124
|
-
},
|
|
125
|
-
settings: {
|
|
126
|
-
...BASE_MANIFEST.screens.settings,
|
|
127
|
-
requires: requirements,
|
|
128
|
-
},
|
|
250
|
+
home: { requires: requirements },
|
|
251
|
+
settings: {},
|
|
129
252
|
},
|
|
130
253
|
};
|
|
131
254
|
}
|
|
132
255
|
|
|
133
|
-
function
|
|
256
|
+
function withAllScreenRequirements(
|
|
257
|
+
requirements: TestScreenRequirements,
|
|
258
|
+
): ExpoRuntimePlanningManifest {
|
|
134
259
|
return {
|
|
135
|
-
...BASE_MANIFEST,
|
|
136
260
|
screens: {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
...BASE_MANIFEST.screens.home,
|
|
140
|
-
requires: requirements,
|
|
141
|
-
},
|
|
261
|
+
home: { requires: requirements },
|
|
262
|
+
settings: { requires: requirements },
|
|
142
263
|
},
|
|
143
264
|
};
|
|
144
265
|
}
|
|
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
|
-
modules: [],
|
|
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;
|