@vxrn/utils 2.0.0-beta.33.1 → 2.0.0-beta.362.1
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/dist/cjs/createMessageSocket.cjs +115 -121
- package/dist/cjs/createMessageSocket.native.cjs +166 -0
- package/dist/cjs/createMessageSocket.native.js +152 -166
- package/dist/cjs/createMessageSocket.native.js.map +1 -1
- package/dist/cjs/detectPackageManager.cjs +76 -72
- package/dist/cjs/detectPackageManager.native.cjs +106 -0
- package/dist/cjs/detectPackageManager.native.js +82 -77
- package/dist/cjs/detectPackageManager.native.js.map +1 -1
- package/dist/cjs/exec.cjs +43 -45
- package/dist/cjs/exec.native.cjs +65 -0
- package/dist/cjs/exec.native.js +48 -49
- package/dist/cjs/exec.native.js.map +1 -1
- package/dist/cjs/index.cjs +10 -11
- package/dist/cjs/index.native.cjs +26 -0
- package/dist/cjs/index.native.js +10 -10
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/mustReplace.cjs +27 -35
- package/dist/cjs/mustReplace.native.cjs +60 -0
- package/dist/cjs/mustReplace.native.js +44 -60
- package/dist/cjs/mustReplace.native.js.map +1 -1
- package/dist/cjs/nativeAppManifest.cjs +328 -0
- package/dist/cjs/nativeAppManifest.native.cjs +445 -0
- package/dist/cjs/nativeAppManifest.native.js +447 -0
- package/dist/cjs/nativeAppManifest.native.js.map +1 -0
- package/dist/cjs/publicEnv.cjs +38 -0
- package/dist/cjs/publicEnv.native.cjs +57 -0
- package/dist/cjs/publicEnv.native.js +59 -0
- package/dist/cjs/publicEnv.native.js.map +1 -0
- package/dist/esm/createMessageSocket.mjs +103 -103
- package/dist/esm/createMessageSocket.mjs.map +1 -1
- package/dist/esm/createMessageSocket.native.js +139 -148
- package/dist/esm/createMessageSocket.native.js.map +1 -1
- package/dist/esm/detectPackageManager.mjs +65 -55
- package/dist/esm/detectPackageManager.mjs.map +1 -1
- package/dist/esm/detectPackageManager.native.js +71 -61
- package/dist/esm/detectPackageManager.native.js.map +1 -1
- package/dist/esm/exec.mjs +29 -27
- package/dist/esm/exec.mjs.map +1 -1
- package/dist/esm/exec.native.js +34 -32
- package/dist/esm/exec.native.js.map +1 -1
- package/dist/esm/index.js +13 -7
- package/dist/esm/index.mjs +13 -7
- package/dist/esm/index.native.js +13 -7
- package/dist/esm/mustReplace.mjs +15 -17
- package/dist/esm/mustReplace.mjs.map +1 -1
- package/dist/esm/mustReplace.native.js +32 -43
- package/dist/esm/mustReplace.native.js.map +1 -1
- package/dist/esm/nativeAppManifest.mjs +305 -0
- package/dist/esm/nativeAppManifest.mjs.map +1 -0
- package/dist/esm/nativeAppManifest.native.js +420 -0
- package/dist/esm/nativeAppManifest.native.js.map +1 -0
- package/dist/esm/publicEnv.mjs +14 -0
- package/dist/esm/publicEnv.mjs.map +1 -0
- package/dist/esm/publicEnv.native.js +31 -0
- package/dist/esm/publicEnv.native.js.map +1 -0
- package/package.json +18 -5
- package/src/index.ts +1 -0
- package/src/nativeAppManifest.test.ts +360 -0
- package/src/nativeAppManifest.ts +702 -0
- package/src/publicEnv.ts +19 -0
- package/types/createMessageSocket.d.ts +1 -1
- package/types/createMessageSocket.d.ts.map +1 -1
- package/types/detectPackageManager.d.ts.map +1 -1
- package/types/exec.d.ts.map +1 -1
- package/types/index.d.ts +1 -0
- package/types/index.d.ts.map +1 -1
- package/types/nativeAppManifest.d.ts +180 -0
- package/types/nativeAppManifest.d.ts.map +1 -0
- package/types/publicEnv.d.ts +9 -0
- package/types/publicEnv.d.ts.map +1 -0
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/index.mjs.map +0 -1
- package/dist/esm/index.native.js.map +0 -1
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
// typed native.app manifest inside the existing one vite options.
|
|
2
|
+
// name is the native target and appregistry key. one canonical definition:
|
|
3
|
+
// one/native re-exports it for apps and vxrn prebuild consumes it, so a new
|
|
4
|
+
// field is added once, here.
|
|
5
|
+
// a property list value, for the ios keys native.app does not model.
|
|
6
|
+
export type PlistValue =
|
|
7
|
+
| string
|
|
8
|
+
| number
|
|
9
|
+
| boolean
|
|
10
|
+
| PlistValue[]
|
|
11
|
+
| { [key: string]: PlistValue }
|
|
12
|
+
|
|
13
|
+
export interface NativeAppManifest {
|
|
14
|
+
name: string
|
|
15
|
+
displayName?: string
|
|
16
|
+
scheme?: string | string[]
|
|
17
|
+
version?: string
|
|
18
|
+
// the phone orientations the app supports, as expo's `orientation`:
|
|
19
|
+
// portrait or landscape pairs, or all four for default. unset keeps the
|
|
20
|
+
// template's portrait-only phone. the ipad list is never narrowed.
|
|
21
|
+
orientation?: 'portrait' | 'landscape' | 'default'
|
|
22
|
+
// light or dark locks the app's appearance; automatic, like unset, follows
|
|
23
|
+
// the system. expo treats unset as light.
|
|
24
|
+
userInterfaceStyle?: 'light' | 'dark' | 'automatic'
|
|
25
|
+
icon?: {
|
|
26
|
+
source: string
|
|
27
|
+
backgroundColor: string
|
|
28
|
+
}
|
|
29
|
+
splash?: {
|
|
30
|
+
source: string
|
|
31
|
+
backgroundColor: string
|
|
32
|
+
width?: number
|
|
33
|
+
// contain (default) centers the artwork at width; cover fills the ios
|
|
34
|
+
// launch screen with it, as expo's resizeMode does. android's system
|
|
35
|
+
// splash shows the centered artwork either way.
|
|
36
|
+
resizeMode?: 'contain' | 'cover'
|
|
37
|
+
// a full-bleed image (a gradient, a texture) under the artwork on the ios
|
|
38
|
+
// launch screen. android's system splash has only a color.
|
|
39
|
+
backgroundImage?: string
|
|
40
|
+
// what a dark-appearance launch shows, as expo's splash.dark: its own
|
|
41
|
+
// background, and optionally its own artwork and background image.
|
|
42
|
+
dark?: {
|
|
43
|
+
source?: string
|
|
44
|
+
backgroundColor: string
|
|
45
|
+
backgroundImage?: string
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// project-relative .ttf or .otf files bundled into the binary, as the
|
|
49
|
+
// expo-font plugin's `fonts`: ios lists them in UIAppFonts, android loads
|
|
50
|
+
// them from assets/fonts. the family name is the file's own.
|
|
51
|
+
fonts?: string[]
|
|
52
|
+
imagePicker?: {
|
|
53
|
+
// ios camera usage description shown at the system prompt. setting it
|
|
54
|
+
// also declares the android camera permission; both are required for
|
|
55
|
+
// One.ImagePicker.launchCamera.
|
|
56
|
+
camera?: string
|
|
57
|
+
}
|
|
58
|
+
// photos permissions for saving and browsing. ios prompts come from
|
|
59
|
+
// these strings; android stamps the media read permissions from
|
|
60
|
+
// readWrite and the add-only marker plus bounded write permission
|
|
61
|
+
// from addOnly.
|
|
62
|
+
photoLibrary?: {
|
|
63
|
+
addOnly?: string
|
|
64
|
+
readWrite?: string
|
|
65
|
+
}
|
|
66
|
+
// Contacts permission prompt for One.Contacts. android stamps the
|
|
67
|
+
// contacts read and write permissions from usage.
|
|
68
|
+
contacts?: {
|
|
69
|
+
usage: string
|
|
70
|
+
}
|
|
71
|
+
// full EventKit access for calendar events and reminders. android
|
|
72
|
+
// stamps the calendar read and write permissions from usage;
|
|
73
|
+
// reminders stay unavailable there.
|
|
74
|
+
calendar?: {
|
|
75
|
+
usage?: string
|
|
76
|
+
remindersUsage?: string
|
|
77
|
+
}
|
|
78
|
+
// core location prompt and opt-in background updates for one.ios.location.
|
|
79
|
+
location?: {
|
|
80
|
+
whenInUse: string
|
|
81
|
+
background?: boolean
|
|
82
|
+
}
|
|
83
|
+
// microphone prompt and background playback for One.Audio. android
|
|
84
|
+
// stamps RECORD_AUDIO from microphone and the media-playback
|
|
85
|
+
// foreground service plus its marker from background.
|
|
86
|
+
audio?: {
|
|
87
|
+
microphone?: string
|
|
88
|
+
background?: boolean
|
|
89
|
+
}
|
|
90
|
+
// usage descriptions for One.Speech dictation, shown at the ios speech
|
|
91
|
+
// recognition and microphone prompts. setting them also declares the
|
|
92
|
+
// android RECORD_AUDIO permission.
|
|
93
|
+
speech?: {
|
|
94
|
+
recognition: string
|
|
95
|
+
microphone: string
|
|
96
|
+
}
|
|
97
|
+
// present when the app uses local notifications. prebuild stamps the
|
|
98
|
+
// android notification permissions and receiver plus the ios delegate gate.
|
|
99
|
+
// push opts into remote push: the fcm source set and the aps-environment
|
|
100
|
+
// entitlement. without it firebase stays out of the app entirely.
|
|
101
|
+
notifications?: {
|
|
102
|
+
push?: boolean
|
|
103
|
+
// the push entitlement's apns environment, development by default. a
|
|
104
|
+
// build signed for testflight or the app store sends through production.
|
|
105
|
+
apsEnvironment?: 'development' | 'production'
|
|
106
|
+
}
|
|
107
|
+
// One.UI.PictureInPicture: stamps the ios audio background mode (pip opens
|
|
108
|
+
// only for a playback session) and android's supportsPictureInPicture.
|
|
109
|
+
pictureInPicture?: boolean
|
|
110
|
+
// One.Updates over-the-air updates. prebuild points the release bundle at
|
|
111
|
+
// the launcher, stamps the url and runtime version into the binary, and
|
|
112
|
+
// writes the embedded manifest beside the release bundle. without url the
|
|
113
|
+
// build launches its embedded bundle with updates disabled.
|
|
114
|
+
updates?: {
|
|
115
|
+
url?: string
|
|
116
|
+
runtimeVersion: string
|
|
117
|
+
}
|
|
118
|
+
ios?: {
|
|
119
|
+
bundleId: string
|
|
120
|
+
buildNumber?: string
|
|
121
|
+
tablet?: boolean
|
|
122
|
+
deploymentTarget?: string
|
|
123
|
+
screensGamma?: boolean
|
|
124
|
+
useFrameworks?: 'static' | 'dynamic'
|
|
125
|
+
ccache?: boolean
|
|
126
|
+
usesNonExemptEncryption?: boolean
|
|
127
|
+
// the tint the system gives the app's controls, tab selection and text
|
|
128
|
+
// cursor, as xcode's AccentColor asset; js reads PlatformColor('AccentColor').
|
|
129
|
+
accentColor?: {
|
|
130
|
+
light: string
|
|
131
|
+
dark?: string
|
|
132
|
+
}
|
|
133
|
+
// extra home screen icons selectable through One.AppIcon. keys are
|
|
134
|
+
// the icon names passed to setIcon; each source is a square 1024px image.
|
|
135
|
+
alternateIcons?: Record<string, { source: string; backgroundColor: string }>
|
|
136
|
+
// system prompt text for Face ID through One.LocalAuthentication.
|
|
137
|
+
faceIdUsageDescription?: string
|
|
138
|
+
// exposes the app's Documents in the Files app and document pickers.
|
|
139
|
+
fileSharing?: boolean
|
|
140
|
+
// universal links and shared web credentials, as entitlement entries
|
|
141
|
+
// such as `applinks:example.com`.
|
|
142
|
+
associatedDomains?: string[]
|
|
143
|
+
// the sign in with apple entitlement, for One.iOS.AppleAuthentication.
|
|
144
|
+
usesAppleSignIn?: boolean
|
|
145
|
+
// a firebase GoogleService-Info.plist, bundled into the app as expo's
|
|
146
|
+
// ios.googleServicesFile does.
|
|
147
|
+
googleServicesFile?: string
|
|
148
|
+
// Info.plist and entitlement keys native.app does not model (a tracking
|
|
149
|
+
// prompt, an sdk's key). a key native.app or the template already writes
|
|
150
|
+
// is rejected: set it through its field instead.
|
|
151
|
+
infoPlist?: Record<string, PlistValue>
|
|
152
|
+
entitlements?: Record<string, PlistValue>
|
|
153
|
+
widgets?: {
|
|
154
|
+
appGroup: string
|
|
155
|
+
kind: string
|
|
156
|
+
displayName: string
|
|
157
|
+
description: string
|
|
158
|
+
pushNotifications?: boolean
|
|
159
|
+
}
|
|
160
|
+
// launch handlers for One.BackgroundTasks. define the matching JS
|
|
161
|
+
// handlers in the native setupFile so a background-only launch can run.
|
|
162
|
+
backgroundTasks?: {
|
|
163
|
+
refresh?: string[]
|
|
164
|
+
processing?: string[]
|
|
165
|
+
}
|
|
166
|
+
// statically compiled App Intents for One.AppIntents. handlers live
|
|
167
|
+
// in the native setupFile so Shortcuts can start the app in the background.
|
|
168
|
+
appIntents?: {
|
|
169
|
+
actions: Array<{
|
|
170
|
+
id: string
|
|
171
|
+
title: string
|
|
172
|
+
textParameterTitle?: string
|
|
173
|
+
shortcutPhrase: string
|
|
174
|
+
}>
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
android?: {
|
|
178
|
+
applicationId: string
|
|
179
|
+
versionCode?: number
|
|
180
|
+
minSdk?: number
|
|
181
|
+
// the android 8+ launcher icon. foreground is a 108dp square image whose
|
|
182
|
+
// artwork sits inside the central 66dp the launcher mask keeps; the
|
|
183
|
+
// background is an image or a color (white when neither is set, as expo);
|
|
184
|
+
// monochrome is the android 13 themed icon.
|
|
185
|
+
adaptiveIcon?: {
|
|
186
|
+
foreground: string
|
|
187
|
+
background?: string
|
|
188
|
+
backgroundColor?: string
|
|
189
|
+
monochrome?: string
|
|
190
|
+
}
|
|
191
|
+
targetSdk?: number
|
|
192
|
+
compileSdk?: number
|
|
193
|
+
// release builds run R8, as expo-build-properties' minify; shrinkResources
|
|
194
|
+
// also drops unused resources and needs minify. proguardRules are appended
|
|
195
|
+
// to the app's proguard-rules.pro.
|
|
196
|
+
minify?: boolean
|
|
197
|
+
shrinkResources?: boolean
|
|
198
|
+
proguardRules?: string
|
|
199
|
+
// extra manifest permissions: a bare name means android.permission.<name>.
|
|
200
|
+
// blocked ones are removed even when a library's manifest merges them in.
|
|
201
|
+
permissions?: string[]
|
|
202
|
+
blockedPermissions?: string[]
|
|
203
|
+
// a firebase google-services.json; prebuild applies the google-services
|
|
204
|
+
// gradle plugin, as expo's android.googleServicesFile does.
|
|
205
|
+
googleServicesFile?: string
|
|
206
|
+
// verified https app links routed to the app (android:autoVerify).
|
|
207
|
+
appLinks?: Array<{ host: string; pathPrefix?: string }>
|
|
208
|
+
// google maps api key for One.UI.Map. setting it compiles the maps sdk
|
|
209
|
+
// into the app and stamps the key meta-data; without it the maps source
|
|
210
|
+
// set stays out and mounting One.UI.Map throws.
|
|
211
|
+
googleMapsApiKey?: string
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const TARGET_NAME = /^[A-Za-z][A-Za-z0-9_]*$/
|
|
216
|
+
const SCHEME = /^[a-z][a-z0-9+.-]*$/i
|
|
217
|
+
const VERSION = /^\d+\.\d+\.\d+/
|
|
218
|
+
const BUILD_NUMBER = /^[A-Za-z0-9.]+$/
|
|
219
|
+
const REVERSE_DNS = /^[A-Za-z][A-Za-z0-9-]*(\.[A-Za-z][A-Za-z0-9-]*)+$/
|
|
220
|
+
const DEPLOYMENT_TARGET = /^\d+\.\d+$/
|
|
221
|
+
const HEX_COLOR = /^#[\da-f]{6}$/i
|
|
222
|
+
const FONT_FILE = /\.(ttf|otf)$/i
|
|
223
|
+
const ORIENTATIONS = ['portrait', 'landscape', 'default'] as const
|
|
224
|
+
const USER_INTERFACE_STYLES = ['light', 'dark', 'automatic'] as const
|
|
225
|
+
|
|
226
|
+
function fail(message: string): never {
|
|
227
|
+
throw new Error(`[one] invalid native.app: ${message}`)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// platform scopes the platform-id requirement: an ios prebuild skips the
|
|
231
|
+
// android requirement and vice versa. without it both are required, because
|
|
232
|
+
// a manifest without platform ids prebuilds nothing.
|
|
233
|
+
export function validateNativeApp(
|
|
234
|
+
manifest: NativeAppManifest,
|
|
235
|
+
platform?: 'ios' | 'android' | string
|
|
236
|
+
): NativeAppManifest {
|
|
237
|
+
if (!manifest || typeof manifest !== 'object') fail('manifest must be an object')
|
|
238
|
+
if (!manifest.name || typeof manifest.name !== 'string') fail('name is required')
|
|
239
|
+
if (!TARGET_NAME.test(manifest.name)) {
|
|
240
|
+
fail(
|
|
241
|
+
`name "${manifest.name}" must start with a letter and contain only letters, digits, and underscore`
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
const schemes =
|
|
245
|
+
manifest.scheme === undefined
|
|
246
|
+
? []
|
|
247
|
+
: Array.isArray(manifest.scheme)
|
|
248
|
+
? manifest.scheme
|
|
249
|
+
: [manifest.scheme]
|
|
250
|
+
for (const scheme of schemes) {
|
|
251
|
+
if (typeof scheme !== 'string' || !SCHEME.test(scheme)) {
|
|
252
|
+
fail(`scheme "${scheme}" must be a valid uri scheme`)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (
|
|
256
|
+
manifest.orientation !== undefined &&
|
|
257
|
+
!ORIENTATIONS.includes(manifest.orientation)
|
|
258
|
+
) {
|
|
259
|
+
fail(`orientation "${manifest.orientation}" must be ${ORIENTATIONS.join(', ')}`)
|
|
260
|
+
}
|
|
261
|
+
if (
|
|
262
|
+
manifest.userInterfaceStyle !== undefined &&
|
|
263
|
+
!USER_INTERFACE_STYLES.includes(manifest.userInterfaceStyle)
|
|
264
|
+
) {
|
|
265
|
+
fail(
|
|
266
|
+
`userInterfaceStyle "${manifest.userInterfaceStyle}" must be ${USER_INTERFACE_STYLES.join(', ')}`
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
if (manifest.fonts !== undefined) {
|
|
270
|
+
const names = new Set<string>()
|
|
271
|
+
for (const font of manifest.fonts) {
|
|
272
|
+
if (typeof font !== 'string' || !FONT_FILE.test(font)) {
|
|
273
|
+
fail(`fonts entry "${font}" must be a .ttf or .otf file path`)
|
|
274
|
+
}
|
|
275
|
+
const name = font.split('/').pop() ?? font
|
|
276
|
+
if (names.has(name)) fail(`fonts lists ${name} twice`)
|
|
277
|
+
names.add(name)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (manifest.version !== undefined && !VERSION.test(manifest.version)) {
|
|
281
|
+
fail(`version "${manifest.version}" must start with major.minor.patch`)
|
|
282
|
+
}
|
|
283
|
+
if (
|
|
284
|
+
manifest.icon !== undefined &&
|
|
285
|
+
(!manifest.icon.source || !HEX_COLOR.test(manifest.icon.backgroundColor))
|
|
286
|
+
) {
|
|
287
|
+
fail('icon requires source and a six-digit hex backgroundColor')
|
|
288
|
+
}
|
|
289
|
+
if (manifest.ios?.alternateIcons !== undefined) {
|
|
290
|
+
if (!manifest.icon) fail('ios.alternateIcons requires a primary icon')
|
|
291
|
+
const entries = Object.entries(manifest.ios.alternateIcons)
|
|
292
|
+
if (entries.length === 0) fail('ios.alternateIcons requires at least one icon')
|
|
293
|
+
for (const [name, icon] of entries) {
|
|
294
|
+
if (name === 'AppIcon' || !TARGET_NAME.test(name)) {
|
|
295
|
+
fail(`ios.alternateIcons name "${name}" must be an identifier other than AppIcon`)
|
|
296
|
+
}
|
|
297
|
+
if (!icon?.source || !HEX_COLOR.test(icon.backgroundColor)) {
|
|
298
|
+
fail(
|
|
299
|
+
`ios.alternateIcons.${name} requires source and a six-digit hex backgroundColor`
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (manifest.ios?.backgroundTasks !== undefined) {
|
|
305
|
+
const tasks = manifest.ios.backgroundTasks
|
|
306
|
+
if (
|
|
307
|
+
!tasks ||
|
|
308
|
+
(tasks.refresh !== undefined && !Array.isArray(tasks.refresh)) ||
|
|
309
|
+
(tasks.processing !== undefined && !Array.isArray(tasks.processing))
|
|
310
|
+
) {
|
|
311
|
+
fail('ios.backgroundTasks refresh and processing must be arrays of identifiers')
|
|
312
|
+
}
|
|
313
|
+
const identifiers = [...(tasks.refresh ?? []), ...(tasks.processing ?? [])]
|
|
314
|
+
if (!identifiers.length || new Set(identifiers).size !== identifiers.length) {
|
|
315
|
+
fail('ios.backgroundTasks needs distinct refresh or processing identifiers')
|
|
316
|
+
}
|
|
317
|
+
for (const identifier of identifiers) {
|
|
318
|
+
if (typeof identifier !== 'string' || !REVERSE_DNS.test(identifier)) {
|
|
319
|
+
fail(
|
|
320
|
+
`ios.backgroundTasks identifier "${identifier}" must use reverse DNS notation`
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (manifest.ios?.appIntents !== undefined) {
|
|
326
|
+
const actions = manifest.ios.appIntents?.actions
|
|
327
|
+
if (!Array.isArray(actions) || actions.length === 0) {
|
|
328
|
+
fail('ios.appIntents.actions must be a non-empty array')
|
|
329
|
+
}
|
|
330
|
+
const ids = new Set<string>()
|
|
331
|
+
for (const action of actions) {
|
|
332
|
+
if (
|
|
333
|
+
!action ||
|
|
334
|
+
typeof action !== 'object' ||
|
|
335
|
+
typeof action.id !== 'string' ||
|
|
336
|
+
!action.id.trim() ||
|
|
337
|
+
typeof action.title !== 'string' ||
|
|
338
|
+
!action.title.trim() ||
|
|
339
|
+
(action.textParameterTitle !== undefined &&
|
|
340
|
+
(typeof action.textParameterTitle !== 'string' ||
|
|
341
|
+
!action.textParameterTitle.trim())) ||
|
|
342
|
+
typeof action.shortcutPhrase !== 'string' ||
|
|
343
|
+
action.shortcutPhrase.split('{app}').length !== 2 ||
|
|
344
|
+
!action.shortcutPhrase.replace('{app}', '').trim()
|
|
345
|
+
) {
|
|
346
|
+
fail(
|
|
347
|
+
'ios.appIntents actions need unique non-empty id/title, optional textParameterTitle, and a shortcutPhrase with one {app}'
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
if (ids.has(action.id)) fail(`ios.appIntents action "${action.id}" is duplicated`)
|
|
351
|
+
ids.add(action.id)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (
|
|
355
|
+
manifest.splash !== undefined &&
|
|
356
|
+
(!manifest.splash.source ||
|
|
357
|
+
!HEX_COLOR.test(manifest.splash.backgroundColor) ||
|
|
358
|
+
(manifest.splash.width !== undefined &&
|
|
359
|
+
(!Number.isFinite(manifest.splash.width) ||
|
|
360
|
+
manifest.splash.width < 1 ||
|
|
361
|
+
manifest.splash.width > 288)))
|
|
362
|
+
) {
|
|
363
|
+
fail(
|
|
364
|
+
'splash requires source, a six-digit hex backgroundColor, and width from 1 to 288'
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
if (
|
|
368
|
+
manifest.splash?.dark !== undefined &&
|
|
369
|
+
!HEX_COLOR.test(manifest.splash.dark.backgroundColor)
|
|
370
|
+
) {
|
|
371
|
+
fail('splash.dark requires a six-digit hex backgroundColor')
|
|
372
|
+
}
|
|
373
|
+
if (
|
|
374
|
+
manifest.splash?.dark?.backgroundImage !== undefined &&
|
|
375
|
+
manifest.splash.backgroundImage === undefined
|
|
376
|
+
) {
|
|
377
|
+
fail('splash.dark.backgroundImage needs splash.backgroundImage for the light launch')
|
|
378
|
+
}
|
|
379
|
+
if (
|
|
380
|
+
manifest.splash?.resizeMode !== undefined &&
|
|
381
|
+
manifest.splash.resizeMode !== 'contain' &&
|
|
382
|
+
manifest.splash.resizeMode !== 'cover'
|
|
383
|
+
) {
|
|
384
|
+
fail(`splash.resizeMode "${manifest.splash.resizeMode}" must be contain or cover`)
|
|
385
|
+
}
|
|
386
|
+
if (
|
|
387
|
+
manifest.imagePicker?.camera !== undefined &&
|
|
388
|
+
(typeof manifest.imagePicker.camera !== 'string' ||
|
|
389
|
+
manifest.imagePicker.camera.trim() === '')
|
|
390
|
+
) {
|
|
391
|
+
fail('imagePicker.camera must be a non-empty string')
|
|
392
|
+
}
|
|
393
|
+
if (
|
|
394
|
+
manifest.photoLibrary !== undefined &&
|
|
395
|
+
(!manifest.photoLibrary ||
|
|
396
|
+
(manifest.photoLibrary.addOnly === undefined &&
|
|
397
|
+
manifest.photoLibrary.readWrite === undefined))
|
|
398
|
+
) {
|
|
399
|
+
fail('photoLibrary must configure addOnly or readWrite')
|
|
400
|
+
}
|
|
401
|
+
if (
|
|
402
|
+
manifest.photoLibrary?.addOnly !== undefined &&
|
|
403
|
+
(typeof manifest.photoLibrary.addOnly !== 'string' ||
|
|
404
|
+
manifest.photoLibrary.addOnly.trim() === '')
|
|
405
|
+
) {
|
|
406
|
+
fail('photoLibrary.addOnly must be a non-empty string')
|
|
407
|
+
}
|
|
408
|
+
if (
|
|
409
|
+
manifest.photoLibrary?.readWrite !== undefined &&
|
|
410
|
+
(typeof manifest.photoLibrary.readWrite !== 'string' ||
|
|
411
|
+
manifest.photoLibrary.readWrite.trim() === '')
|
|
412
|
+
) {
|
|
413
|
+
fail('photoLibrary.readWrite must be a non-empty string')
|
|
414
|
+
}
|
|
415
|
+
if (
|
|
416
|
+
manifest.contacts !== undefined &&
|
|
417
|
+
(!manifest.contacts ||
|
|
418
|
+
typeof manifest.contacts.usage !== 'string' ||
|
|
419
|
+
manifest.contacts.usage.trim() === '')
|
|
420
|
+
) {
|
|
421
|
+
fail('contacts.usage must be a non-empty string')
|
|
422
|
+
}
|
|
423
|
+
if (manifest.calendar !== undefined) {
|
|
424
|
+
if (
|
|
425
|
+
!manifest.calendar ||
|
|
426
|
+
(manifest.calendar.usage === undefined &&
|
|
427
|
+
manifest.calendar.remindersUsage === undefined)
|
|
428
|
+
) {
|
|
429
|
+
fail('calendar.usage or calendar.remindersUsage must be a non-empty string')
|
|
430
|
+
}
|
|
431
|
+
if (
|
|
432
|
+
manifest.calendar.usage !== undefined &&
|
|
433
|
+
(typeof manifest.calendar.usage !== 'string' ||
|
|
434
|
+
manifest.calendar.usage.trim() === '')
|
|
435
|
+
) {
|
|
436
|
+
fail('calendar.usage must be a non-empty string')
|
|
437
|
+
}
|
|
438
|
+
if (
|
|
439
|
+
manifest.calendar.remindersUsage !== undefined &&
|
|
440
|
+
(typeof manifest.calendar.remindersUsage !== 'string' ||
|
|
441
|
+
manifest.calendar.remindersUsage.trim() === '')
|
|
442
|
+
) {
|
|
443
|
+
fail('calendar.remindersUsage must be a non-empty string')
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (
|
|
447
|
+
manifest.location !== undefined &&
|
|
448
|
+
(!manifest.location ||
|
|
449
|
+
typeof manifest.location.whenInUse !== 'string' ||
|
|
450
|
+
manifest.location.whenInUse.trim() === '')
|
|
451
|
+
) {
|
|
452
|
+
fail('location.whenInUse must be a non-empty string')
|
|
453
|
+
}
|
|
454
|
+
if (
|
|
455
|
+
manifest.location?.background !== undefined &&
|
|
456
|
+
typeof manifest.location.background !== 'boolean'
|
|
457
|
+
) {
|
|
458
|
+
fail('location.background must be a boolean')
|
|
459
|
+
}
|
|
460
|
+
if (manifest.audio !== undefined) {
|
|
461
|
+
if (!manifest.audio || typeof manifest.audio !== 'object') {
|
|
462
|
+
fail('audio must configure microphone or background playback')
|
|
463
|
+
}
|
|
464
|
+
if (
|
|
465
|
+
manifest.audio.microphone !== undefined &&
|
|
466
|
+
(typeof manifest.audio.microphone !== 'string' ||
|
|
467
|
+
manifest.audio.microphone.trim() === '')
|
|
468
|
+
) {
|
|
469
|
+
fail('audio.microphone must be a non-empty string')
|
|
470
|
+
}
|
|
471
|
+
if (
|
|
472
|
+
manifest.audio.background !== undefined &&
|
|
473
|
+
typeof manifest.audio.background !== 'boolean'
|
|
474
|
+
) {
|
|
475
|
+
fail('audio.background must be a boolean')
|
|
476
|
+
}
|
|
477
|
+
if (manifest.audio.microphone === undefined && manifest.audio.background !== true) {
|
|
478
|
+
fail('audio must configure microphone or background playback')
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (
|
|
482
|
+
manifest.ios?.faceIdUsageDescription !== undefined &&
|
|
483
|
+
(typeof manifest.ios.faceIdUsageDescription !== 'string' ||
|
|
484
|
+
manifest.ios.faceIdUsageDescription.trim() === '')
|
|
485
|
+
) {
|
|
486
|
+
fail('ios.faceIdUsageDescription must be a non-empty string')
|
|
487
|
+
}
|
|
488
|
+
if (
|
|
489
|
+
manifest.speech !== undefined &&
|
|
490
|
+
(typeof manifest.speech?.recognition !== 'string' ||
|
|
491
|
+
manifest.speech.recognition.trim() === '' ||
|
|
492
|
+
typeof manifest.speech.microphone !== 'string' ||
|
|
493
|
+
manifest.speech.microphone.trim() === '')
|
|
494
|
+
) {
|
|
495
|
+
fail('speech.recognition and speech.microphone must be non-empty strings')
|
|
496
|
+
}
|
|
497
|
+
if (manifest.notifications !== undefined) {
|
|
498
|
+
if (
|
|
499
|
+
typeof manifest.notifications !== 'object' ||
|
|
500
|
+
(manifest.notifications.push !== undefined &&
|
|
501
|
+
typeof manifest.notifications.push !== 'boolean')
|
|
502
|
+
) {
|
|
503
|
+
fail('notifications.push must be a boolean')
|
|
504
|
+
}
|
|
505
|
+
const aps = manifest.notifications.apsEnvironment
|
|
506
|
+
if (aps !== undefined && aps !== 'development' && aps !== 'production') {
|
|
507
|
+
fail(`notifications.apsEnvironment "${aps}" must be development or production`)
|
|
508
|
+
}
|
|
509
|
+
if (aps !== undefined && !manifest.notifications.push) {
|
|
510
|
+
fail('notifications.apsEnvironment needs notifications.push')
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const accent = manifest.ios?.accentColor
|
|
514
|
+
if (
|
|
515
|
+
accent !== undefined &&
|
|
516
|
+
(!HEX_COLOR.test(accent.light) ||
|
|
517
|
+
(accent.dark !== undefined && !HEX_COLOR.test(accent.dark)))
|
|
518
|
+
) {
|
|
519
|
+
fail('ios.accentColor light and dark must be six-digit hex colors')
|
|
520
|
+
}
|
|
521
|
+
if (
|
|
522
|
+
manifest.pictureInPicture !== undefined &&
|
|
523
|
+
typeof manifest.pictureInPicture !== 'boolean'
|
|
524
|
+
) {
|
|
525
|
+
fail('pictureInPicture must be a boolean')
|
|
526
|
+
}
|
|
527
|
+
if (manifest.updates !== undefined) {
|
|
528
|
+
if (
|
|
529
|
+
typeof manifest.updates.runtimeVersion !== 'string' ||
|
|
530
|
+
manifest.updates.runtimeVersion.trim() === ''
|
|
531
|
+
) {
|
|
532
|
+
fail('updates.runtimeVersion must be a non-empty string')
|
|
533
|
+
}
|
|
534
|
+
if (
|
|
535
|
+
manifest.updates.url !== undefined &&
|
|
536
|
+
(typeof manifest.updates.url !== 'string' || manifest.updates.url.trim() === '')
|
|
537
|
+
) {
|
|
538
|
+
fail('updates.url must be a non-empty string')
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (!platform || platform === 'ios') {
|
|
542
|
+
if (!manifest.ios?.bundleId || !REVERSE_DNS.test(manifest.ios.bundleId)) {
|
|
543
|
+
fail(`ios.bundleId "${manifest.ios?.bundleId}" must be reverse-dns`)
|
|
544
|
+
}
|
|
545
|
+
if (
|
|
546
|
+
manifest.ios.deploymentTarget !== undefined &&
|
|
547
|
+
!DEPLOYMENT_TARGET.test(manifest.ios.deploymentTarget)
|
|
548
|
+
) {
|
|
549
|
+
fail(
|
|
550
|
+
`ios.deploymentTarget "${manifest.ios.deploymentTarget}" must look like "17.0"`
|
|
551
|
+
)
|
|
552
|
+
}
|
|
553
|
+
if (
|
|
554
|
+
manifest.ios.buildNumber !== undefined &&
|
|
555
|
+
!BUILD_NUMBER.test(manifest.ios.buildNumber)
|
|
556
|
+
) {
|
|
557
|
+
fail(
|
|
558
|
+
`ios.buildNumber "${manifest.ios.buildNumber}" must contain only letters, digits, and dots`
|
|
559
|
+
)
|
|
560
|
+
}
|
|
561
|
+
const widgets = manifest.ios.widgets
|
|
562
|
+
if (widgets) {
|
|
563
|
+
if (!manifest.ios.deploymentTarget || Number(manifest.ios.deploymentTarget) < 17) {
|
|
564
|
+
fail('ios.widgets requires ios.deploymentTarget of 17.0 or newer')
|
|
565
|
+
}
|
|
566
|
+
if (
|
|
567
|
+
!/^group\.[A-Za-z][A-Za-z0-9-]*(\.[A-Za-z][A-Za-z0-9-]*)+$/.test(widgets.appGroup)
|
|
568
|
+
) {
|
|
569
|
+
fail('ios.widgets.appGroup must be a reverse-dns App Group beginning with group.')
|
|
570
|
+
}
|
|
571
|
+
if (!/^[A-Za-z][A-Za-z0-9._-]*$/.test(widgets.kind)) {
|
|
572
|
+
fail('ios.widgets.kind must be a non-empty WidgetKit kind')
|
|
573
|
+
}
|
|
574
|
+
if (!widgets.displayName?.trim() || !widgets.description?.trim()) {
|
|
575
|
+
fail('ios.widgets.displayName and description must be non-empty strings')
|
|
576
|
+
}
|
|
577
|
+
if (
|
|
578
|
+
widgets.pushNotifications !== undefined &&
|
|
579
|
+
typeof widgets.pushNotifications !== 'boolean'
|
|
580
|
+
) {
|
|
581
|
+
fail('ios.widgets.pushNotifications must be a boolean')
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (!platform || platform === 'android') {
|
|
586
|
+
if (
|
|
587
|
+
!manifest.android?.applicationId ||
|
|
588
|
+
!REVERSE_DNS.test(manifest.android.applicationId)
|
|
589
|
+
) {
|
|
590
|
+
fail(
|
|
591
|
+
`android.applicationId "${manifest.android?.applicationId}" must be reverse-dns`
|
|
592
|
+
)
|
|
593
|
+
}
|
|
594
|
+
if (
|
|
595
|
+
manifest.android.minSdk !== undefined &&
|
|
596
|
+
(!Number.isInteger(manifest.android.minSdk) ||
|
|
597
|
+
manifest.android.minSdk < 21 ||
|
|
598
|
+
manifest.android.minSdk > 36)
|
|
599
|
+
) {
|
|
600
|
+
fail(`android.minSdk "${manifest.android.minSdk}" must be an integer from 21 to 36`)
|
|
601
|
+
}
|
|
602
|
+
if (
|
|
603
|
+
manifest.android.versionCode !== undefined &&
|
|
604
|
+
(!Number.isInteger(manifest.android.versionCode) ||
|
|
605
|
+
manifest.android.versionCode < 1)
|
|
606
|
+
) {
|
|
607
|
+
fail(
|
|
608
|
+
`android.versionCode "${manifest.android.versionCode}" must be a positive integer`
|
|
609
|
+
)
|
|
610
|
+
}
|
|
611
|
+
for (const key of ['targetSdk', 'compileSdk'] as const) {
|
|
612
|
+
const value = manifest.android[key]
|
|
613
|
+
if (value !== undefined && (!Number.isInteger(value) || value < 21)) {
|
|
614
|
+
fail(`android.${key} "${value}" must be an api level integer`)
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (manifest.android.shrinkResources && !manifest.android.minify) {
|
|
618
|
+
fail('android.shrinkResources needs android.minify')
|
|
619
|
+
}
|
|
620
|
+
for (const link of manifest.android.appLinks ?? []) {
|
|
621
|
+
if (!link.host || !REVERSE_DNS.test(link.host)) {
|
|
622
|
+
fail(`android.appLinks host "${link.host}" must be a domain`)
|
|
623
|
+
}
|
|
624
|
+
if (link.pathPrefix !== undefined && !link.pathPrefix.startsWith('/')) {
|
|
625
|
+
fail(`android.appLinks pathPrefix "${link.pathPrefix}" must start with /`)
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const adaptiveIcon = manifest.android.adaptiveIcon
|
|
629
|
+
if (
|
|
630
|
+
adaptiveIcon !== undefined &&
|
|
631
|
+
(!adaptiveIcon.foreground ||
|
|
632
|
+
(adaptiveIcon.backgroundColor !== undefined &&
|
|
633
|
+
!HEX_COLOR.test(adaptiveIcon.backgroundColor)))
|
|
634
|
+
) {
|
|
635
|
+
fail(
|
|
636
|
+
'android.adaptiveIcon requires foreground, and backgroundColor must be six-digit hex'
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
if (
|
|
640
|
+
manifest.android.googleMapsApiKey !== undefined &&
|
|
641
|
+
(typeof manifest.android.googleMapsApiKey !== 'string' ||
|
|
642
|
+
manifest.android.googleMapsApiKey.trim() === '')
|
|
643
|
+
) {
|
|
644
|
+
fail('android.googleMapsApiKey must be a non-empty string')
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return manifest
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// the expo config shape the dev server's manifest carries (`extra.expoClient`)
|
|
651
|
+
// for an app that declares native.app and no expo: clients that read an app's
|
|
652
|
+
// name, scheme or splash from the manifest read a one app the same way.
|
|
653
|
+
export function expoClientFromNativeApp(app: NativeAppManifest) {
|
|
654
|
+
return {
|
|
655
|
+
name: app.displayName ?? app.name,
|
|
656
|
+
slug: app.name,
|
|
657
|
+
scheme: app.scheme,
|
|
658
|
+
version: app.version,
|
|
659
|
+
orientation: app.orientation,
|
|
660
|
+
// an expo config without the key means light, while prebuild writes no
|
|
661
|
+
// UIUserInterfaceStyle for an unset style and the app follows the system,
|
|
662
|
+
// so the manifest states the style the built app has.
|
|
663
|
+
userInterfaceStyle: app.userInterfaceStyle ?? 'automatic',
|
|
664
|
+
icon: app.icon?.source,
|
|
665
|
+
splash: app.splash && {
|
|
666
|
+
image: app.splash.source,
|
|
667
|
+
backgroundImage: app.splash.backgroundImage,
|
|
668
|
+
backgroundColor: app.splash.backgroundColor,
|
|
669
|
+
imageWidth: app.splash.width,
|
|
670
|
+
resizeMode: app.splash.resizeMode,
|
|
671
|
+
dark: app.splash.dark && {
|
|
672
|
+
image: app.splash.dark.source,
|
|
673
|
+
backgroundImage: app.splash.dark.backgroundImage,
|
|
674
|
+
backgroundColor: app.splash.dark.backgroundColor,
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
plugins: app.fonts?.length ? [['expo-font', { fonts: app.fonts }]] : undefined,
|
|
678
|
+
ios: app.ios && {
|
|
679
|
+
bundleIdentifier: app.ios.bundleId,
|
|
680
|
+
buildNumber: app.ios.buildNumber,
|
|
681
|
+
supportsTablet: app.ios.tablet,
|
|
682
|
+
associatedDomains: app.ios.associatedDomains,
|
|
683
|
+
usesAppleSignIn: app.ios.usesAppleSignIn,
|
|
684
|
+
googleServicesFile: app.ios.googleServicesFile,
|
|
685
|
+
infoPlist: app.ios.infoPlist,
|
|
686
|
+
entitlements: app.ios.entitlements,
|
|
687
|
+
},
|
|
688
|
+
android: app.android && {
|
|
689
|
+
package: app.android.applicationId,
|
|
690
|
+
versionCode: app.android.versionCode,
|
|
691
|
+
permissions: app.android.permissions,
|
|
692
|
+
blockedPermissions: app.android.blockedPermissions,
|
|
693
|
+
googleServicesFile: app.android.googleServicesFile,
|
|
694
|
+
adaptiveIcon: app.android.adaptiveIcon && {
|
|
695
|
+
foregroundImage: app.android.adaptiveIcon.foreground,
|
|
696
|
+
backgroundImage: app.android.adaptiveIcon.background,
|
|
697
|
+
backgroundColor: app.android.adaptiveIcon.backgroundColor,
|
|
698
|
+
monochromeImage: app.android.adaptiveIcon.monochrome,
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
}
|
|
702
|
+
}
|