@vxrn/utils 2.0.0-beta.99.1 → 2.6.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.
Files changed (34) hide show
  1. package/dist/cjs/createMessageSocket.native.cjs +1 -1
  2. package/dist/cjs/createMessageSocket.native.js +1 -1
  3. package/dist/cjs/createMessageSocket.native.js.map +1 -1
  4. package/dist/cjs/detectPackageManager.cjs +3 -2
  5. package/dist/cjs/detectPackageManager.native.cjs +5 -4
  6. package/dist/cjs/detectPackageManager.native.js +5 -4
  7. package/dist/cjs/detectPackageManager.native.js.map +1 -1
  8. package/dist/cjs/exec.native.cjs +3 -3
  9. package/dist/cjs/exec.native.js +3 -3
  10. package/dist/cjs/exec.native.js.map +1 -1
  11. package/dist/cjs/nativeAppManifest.cjs +231 -1
  12. package/dist/cjs/nativeAppManifest.native.cjs +328 -6
  13. package/dist/cjs/nativeAppManifest.native.js +328 -6
  14. package/dist/cjs/nativeAppManifest.native.js.map +1 -1
  15. package/dist/esm/createMessageSocket.native.js +1 -1
  16. package/dist/esm/createMessageSocket.native.js.map +1 -1
  17. package/dist/esm/detectPackageManager.mjs +3 -2
  18. package/dist/esm/detectPackageManager.mjs.map +1 -1
  19. package/dist/esm/detectPackageManager.native.js +5 -4
  20. package/dist/esm/detectPackageManager.native.js.map +1 -1
  21. package/dist/esm/exec.native.js +3 -3
  22. package/dist/esm/exec.native.js.map +1 -1
  23. package/dist/esm/nativeAppManifest.mjs +228 -1
  24. package/dist/esm/nativeAppManifest.mjs.map +1 -1
  25. package/dist/esm/nativeAppManifest.native.js +325 -6
  26. package/dist/esm/nativeAppManifest.native.js.map +1 -1
  27. package/package.json +5 -4
  28. package/src/createMessageSocket.ts +1 -1
  29. package/src/detectPackageManager.ts +3 -2
  30. package/src/nativeAppManifest.test.ts +180 -1
  31. package/src/nativeAppManifest.ts +517 -1
  32. package/types/detectPackageManager.d.ts.map +1 -1
  33. package/types/nativeAppManifest.d.ts +135 -1
  34. package/types/nativeAppManifest.d.ts.map +1 -1
@@ -2,11 +2,26 @@
2
2
  // name is the native target and appregistry key. one canonical definition:
3
3
  // one/native re-exports it for apps and vxrn prebuild consumes it, so a new
4
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
+
5
13
  export interface NativeAppManifest {
6
14
  name: string
7
15
  displayName?: string
8
16
  scheme?: string | string[]
9
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'
10
25
  icon?: {
11
26
  source: string
12
27
  backgroundColor: string
@@ -15,12 +30,88 @@ export interface NativeAppManifest {
15
30
  source: string
16
31
  backgroundColor: string
17
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
+ }
18
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[]
19
52
  imagePicker?: {
20
53
  // ios camera usage description shown at the system prompt. setting it
21
54
  // also declares the android camera permission; both are required for
22
55
  // One.ImagePicker.launchCamera.
23
56
  camera?: string
57
+ // ios microphone usage description for camera video, whose recording
58
+ // carries audio. required for launchCamera with videos on ios; android
59
+ // needs nothing, since the camera app records.
60
+ microphone?: string
61
+ }
62
+ // photos permissions for saving and browsing. ios prompts come from
63
+ // these strings; android stamps the media read permissions from
64
+ // readWrite and the add-only marker plus bounded write permission
65
+ // from addOnly.
66
+ photoLibrary?: {
67
+ addOnly?: string
68
+ readWrite?: string
69
+ }
70
+ // Contacts permission prompt for One.Contacts. android stamps the
71
+ // contacts read and write permissions from usage.
72
+ contacts?: {
73
+ usage: string
74
+ }
75
+ // full EventKit access for calendar events and reminders. android
76
+ // stamps the calendar read and write permissions from usage;
77
+ // reminders stay unavailable there.
78
+ calendar?: {
79
+ usage?: string
80
+ remindersUsage?: string
81
+ }
82
+ // core location prompt and opt-in background updates for one.ios.location.
83
+ location?: {
84
+ whenInUse: string
85
+ // ios always prompt for region monitoring and background delivery
86
+ // through One.Location.requestAlwaysPermission. without it the
87
+ // always request rejects with E_LOCATION_MANIFEST.
88
+ alwaysAndWhenInUse?: string
89
+ background?: boolean
90
+ }
91
+ // microphone prompt and background playback for One.Audio. android
92
+ // stamps RECORD_AUDIO from microphone and the media-playback
93
+ // foreground service plus its marker from background.
94
+ audio?: {
95
+ microphone?: string
96
+ background?: boolean
97
+ }
98
+ // usage descriptions for One.Speech dictation, shown at the ios speech
99
+ // recognition and microphone prompts. setting them also declares the
100
+ // android RECORD_AUDIO permission.
101
+ speech?: {
102
+ recognition: string
103
+ microphone: string
104
+ }
105
+ // system brightness writes for One.Brightness on android. setting it
106
+ // stamps the WRITE_SETTINGS permission; without it setSystem and
107
+ // setSystemMode reject with E_BRIGHTNESS_PERMISSIONS.
108
+ brightness?: boolean
109
+ // motion prompt for One.Pedometer step counting. ios stamps
110
+ // NSMotionUsageDescription from usage (the barometer reads it too);
111
+ // android stamps the activity-recognition permission. without it
112
+ // requestPermissions rejects with E_PEDOMETER_MANIFEST.
113
+ pedometer?: {
114
+ usage: string
24
115
  }
25
116
  // present when the app uses local notifications. prebuild stamps the
26
117
  // android notification permissions and receiver plus the ios delegate gate.
@@ -28,10 +119,21 @@ export interface NativeAppManifest {
28
119
  // entitlement. without it firebase stays out of the app entirely.
29
120
  notifications?: {
30
121
  push?: boolean
122
+ // the push entitlement's apns environment, development by default. a
123
+ // build signed for testflight or the app store sends through production.
124
+ apsEnvironment?: 'development' | 'production'
31
125
  }
32
126
  // One.UI.PictureInPicture: stamps the ios audio background mode (pip opens
33
127
  // only for a playback session) and android's supportsPictureInPicture.
34
128
  pictureInPicture?: boolean
129
+ // One.Updates over-the-air updates. prebuild points the release bundle at
130
+ // the launcher, stamps the url and runtime version into the binary, and
131
+ // writes the embedded manifest beside the release bundle. without url the
132
+ // build launches its embedded bundle with updates disabled.
133
+ updates?: {
134
+ url?: string
135
+ runtimeVersion: string
136
+ }
35
137
  ios?: {
36
138
  bundleId: string
37
139
  buildNumber?: string
@@ -41,8 +143,32 @@ export interface NativeAppManifest {
41
143
  useFrameworks?: 'static' | 'dynamic'
42
144
  ccache?: boolean
43
145
  usesNonExemptEncryption?: boolean
146
+ // the tint the system gives the app's controls, tab selection and text
147
+ // cursor, as xcode's AccentColor asset; js reads PlatformColor('AccentColor').
148
+ accentColor?: {
149
+ light: string
150
+ dark?: string
151
+ }
152
+ // extra home screen icons selectable through One.AppIcon. keys are
153
+ // the icon names passed to setIcon; each source is a square 1024px image.
154
+ alternateIcons?: Record<string, { source: string; backgroundColor: string }>
155
+ // system prompt text for Face ID through One.LocalAuthentication.
156
+ faceIdUsageDescription?: string
44
157
  // exposes the app's Documents in the Files app and document pickers.
45
158
  fileSharing?: boolean
159
+ // universal links and shared web credentials, as entitlement entries
160
+ // such as `applinks:example.com`.
161
+ associatedDomains?: string[]
162
+ // the sign in with apple entitlement, for One.iOS.AppleAuthentication.
163
+ usesAppleSignIn?: boolean
164
+ // a firebase GoogleService-Info.plist, bundled into the app as expo's
165
+ // ios.googleServicesFile does.
166
+ googleServicesFile?: string
167
+ // Info.plist and entitlement keys native.app does not model (a tracking
168
+ // prompt, an sdk's key). a key native.app or the template already writes
169
+ // is rejected: set it through its field instead.
170
+ infoPlist?: Record<string, PlistValue>
171
+ entitlements?: Record<string, PlistValue>
46
172
  widgets?: {
47
173
  appGroup: string
48
174
  kind: string
@@ -50,15 +176,54 @@ export interface NativeAppManifest {
50
176
  description: string
51
177
  pushNotifications?: boolean
52
178
  }
179
+ // launch handlers for One.BackgroundTasks. define the matching JS
180
+ // handlers in the native setupFile so a background-only launch can run.
181
+ backgroundTasks?: {
182
+ refresh?: string[]
183
+ processing?: string[]
184
+ }
185
+ // statically compiled App Intents for One.AppIntents. handlers live
186
+ // in the native setupFile so Shortcuts can start the app in the background.
187
+ appIntents?: {
188
+ actions: Array<{
189
+ id: string
190
+ title: string
191
+ textParameterTitle?: string
192
+ shortcutPhrase: string
193
+ }>
194
+ }
53
195
  }
54
196
  android?: {
55
197
  applicationId: string
56
198
  versionCode?: number
57
199
  minSdk?: number
200
+ // the android 8+ launcher icon. foreground is a 108dp square image whose
201
+ // artwork sits inside the central 66dp the launcher mask keeps; the
202
+ // background is an image or a color (white when neither is set, as expo);
203
+ // monochrome is the android 13 themed icon.
58
204
  adaptiveIcon?: {
59
- foreground?: string
205
+ foreground: string
60
206
  background?: string
207
+ backgroundColor?: string
208
+ monochrome?: string
61
209
  }
210
+ targetSdk?: number
211
+ compileSdk?: number
212
+ // release builds run R8, as expo-build-properties' minify; shrinkResources
213
+ // also drops unused resources and needs minify. proguardRules are appended
214
+ // to the app's proguard-rules.pro.
215
+ minify?: boolean
216
+ shrinkResources?: boolean
217
+ proguardRules?: string
218
+ // extra manifest permissions: a bare name means android.permission.<name>.
219
+ // blocked ones are removed even when a library's manifest merges them in.
220
+ permissions?: string[]
221
+ blockedPermissions?: string[]
222
+ // a firebase google-services.json; prebuild applies the google-services
223
+ // gradle plugin, as expo's android.googleServicesFile does.
224
+ googleServicesFile?: string
225
+ // verified https app links routed to the app (android:autoVerify).
226
+ appLinks?: Array<{ host: string; pathPrefix?: string }>
62
227
  // google maps api key for One.UI.Map. setting it compiles the maps sdk
63
228
  // into the app and stamps the key meta-data; without it the maps source
64
229
  // set stays out and mounting One.UI.Map throws.
@@ -73,6 +238,9 @@ const BUILD_NUMBER = /^[A-Za-z0-9.]+$/
73
238
  const REVERSE_DNS = /^[A-Za-z][A-Za-z0-9-]*(\.[A-Za-z][A-Za-z0-9-]*)+$/
74
239
  const DEPLOYMENT_TARGET = /^\d+\.\d+$/
75
240
  const HEX_COLOR = /^#[\da-f]{6}$/i
241
+ const FONT_FILE = /\.(ttf|otf)$/i
242
+ const ORIENTATIONS = ['portrait', 'landscape', 'default'] as const
243
+ const USER_INTERFACE_STYLES = ['light', 'dark', 'automatic'] as const
76
244
 
77
245
  function fail(message: string): never {
78
246
  throw new Error(`[one] invalid native.app: ${message}`)
@@ -103,6 +271,31 @@ export function validateNativeApp(
103
271
  fail(`scheme "${scheme}" must be a valid uri scheme`)
104
272
  }
105
273
  }
274
+ if (
275
+ manifest.orientation !== undefined &&
276
+ !ORIENTATIONS.includes(manifest.orientation)
277
+ ) {
278
+ fail(`orientation "${manifest.orientation}" must be ${ORIENTATIONS.join(', ')}`)
279
+ }
280
+ if (
281
+ manifest.userInterfaceStyle !== undefined &&
282
+ !USER_INTERFACE_STYLES.includes(manifest.userInterfaceStyle)
283
+ ) {
284
+ fail(
285
+ `userInterfaceStyle "${manifest.userInterfaceStyle}" must be ${USER_INTERFACE_STYLES.join(', ')}`
286
+ )
287
+ }
288
+ if (manifest.fonts !== undefined) {
289
+ const names = new Set<string>()
290
+ for (const font of manifest.fonts) {
291
+ if (typeof font !== 'string' || !FONT_FILE.test(font)) {
292
+ fail(`fonts entry "${font}" must be a .ttf or .otf file path`)
293
+ }
294
+ const name = font.split('/').pop() ?? font
295
+ if (names.has(name)) fail(`fonts lists ${name} twice`)
296
+ names.add(name)
297
+ }
298
+ }
106
299
  if (manifest.version !== undefined && !VERSION.test(manifest.version)) {
107
300
  fail(`version "${manifest.version}" must start with major.minor.patch`)
108
301
  }
@@ -112,6 +305,71 @@ export function validateNativeApp(
112
305
  ) {
113
306
  fail('icon requires source and a six-digit hex backgroundColor')
114
307
  }
308
+ if (manifest.ios?.alternateIcons !== undefined) {
309
+ if (!manifest.icon) fail('ios.alternateIcons requires a primary icon')
310
+ const entries = Object.entries(manifest.ios.alternateIcons)
311
+ if (entries.length === 0) fail('ios.alternateIcons requires at least one icon')
312
+ for (const [name, icon] of entries) {
313
+ if (name === 'AppIcon' || !TARGET_NAME.test(name)) {
314
+ fail(`ios.alternateIcons name "${name}" must be an identifier other than AppIcon`)
315
+ }
316
+ if (!icon?.source || !HEX_COLOR.test(icon.backgroundColor)) {
317
+ fail(
318
+ `ios.alternateIcons.${name} requires source and a six-digit hex backgroundColor`
319
+ )
320
+ }
321
+ }
322
+ }
323
+ if (manifest.ios?.backgroundTasks !== undefined) {
324
+ const tasks = manifest.ios.backgroundTasks
325
+ if (
326
+ !tasks ||
327
+ (tasks.refresh !== undefined && !Array.isArray(tasks.refresh)) ||
328
+ (tasks.processing !== undefined && !Array.isArray(tasks.processing))
329
+ ) {
330
+ fail('ios.backgroundTasks refresh and processing must be arrays of identifiers')
331
+ }
332
+ const identifiers = [...(tasks.refresh ?? []), ...(tasks.processing ?? [])]
333
+ if (!identifiers.length || new Set(identifiers).size !== identifiers.length) {
334
+ fail('ios.backgroundTasks needs distinct refresh or processing identifiers')
335
+ }
336
+ for (const identifier of identifiers) {
337
+ if (typeof identifier !== 'string' || !REVERSE_DNS.test(identifier)) {
338
+ fail(
339
+ `ios.backgroundTasks identifier "${identifier}" must use reverse DNS notation`
340
+ )
341
+ }
342
+ }
343
+ }
344
+ if (manifest.ios?.appIntents !== undefined) {
345
+ const actions = manifest.ios.appIntents?.actions
346
+ if (!Array.isArray(actions) || actions.length === 0) {
347
+ fail('ios.appIntents.actions must be a non-empty array')
348
+ }
349
+ const ids = new Set<string>()
350
+ for (const action of actions) {
351
+ if (
352
+ !action ||
353
+ typeof action !== 'object' ||
354
+ typeof action.id !== 'string' ||
355
+ !action.id.trim() ||
356
+ typeof action.title !== 'string' ||
357
+ !action.title.trim() ||
358
+ (action.textParameterTitle !== undefined &&
359
+ (typeof action.textParameterTitle !== 'string' ||
360
+ !action.textParameterTitle.trim())) ||
361
+ typeof action.shortcutPhrase !== 'string' ||
362
+ action.shortcutPhrase.split('{app}').length !== 2 ||
363
+ !action.shortcutPhrase.replace('{app}', '').trim()
364
+ ) {
365
+ fail(
366
+ 'ios.appIntents actions need unique non-empty id/title, optional textParameterTitle, and a shortcutPhrase with one {app}'
367
+ )
368
+ }
369
+ if (ids.has(action.id)) fail(`ios.appIntents action "${action.id}" is duplicated`)
370
+ ids.add(action.id)
371
+ }
372
+ }
115
373
  if (
116
374
  manifest.splash !== undefined &&
117
375
  (!manifest.splash.source ||
@@ -125,6 +383,25 @@ export function validateNativeApp(
125
383
  'splash requires source, a six-digit hex backgroundColor, and width from 1 to 288'
126
384
  )
127
385
  }
386
+ if (
387
+ manifest.splash?.dark !== undefined &&
388
+ !HEX_COLOR.test(manifest.splash.dark.backgroundColor)
389
+ ) {
390
+ fail('splash.dark requires a six-digit hex backgroundColor')
391
+ }
392
+ if (
393
+ manifest.splash?.dark?.backgroundImage !== undefined &&
394
+ manifest.splash.backgroundImage === undefined
395
+ ) {
396
+ fail('splash.dark.backgroundImage needs splash.backgroundImage for the light launch')
397
+ }
398
+ if (
399
+ manifest.splash?.resizeMode !== undefined &&
400
+ manifest.splash.resizeMode !== 'contain' &&
401
+ manifest.splash.resizeMode !== 'cover'
402
+ ) {
403
+ fail(`splash.resizeMode "${manifest.splash.resizeMode}" must be contain or cover`)
404
+ }
128
405
  if (
129
406
  manifest.imagePicker?.camera !== undefined &&
130
407
  (typeof manifest.imagePicker.camera !== 'string' ||
@@ -132,6 +409,134 @@ export function validateNativeApp(
132
409
  ) {
133
410
  fail('imagePicker.camera must be a non-empty string')
134
411
  }
412
+ if (
413
+ manifest.imagePicker?.microphone !== undefined &&
414
+ (typeof manifest.imagePicker.microphone !== 'string' ||
415
+ manifest.imagePicker.microphone.trim() === '')
416
+ ) {
417
+ fail('imagePicker.microphone must be a non-empty string')
418
+ }
419
+ if (
420
+ manifest.photoLibrary !== undefined &&
421
+ (!manifest.photoLibrary ||
422
+ (manifest.photoLibrary.addOnly === undefined &&
423
+ manifest.photoLibrary.readWrite === undefined))
424
+ ) {
425
+ fail('photoLibrary must configure addOnly or readWrite')
426
+ }
427
+ if (
428
+ manifest.photoLibrary?.addOnly !== undefined &&
429
+ (typeof manifest.photoLibrary.addOnly !== 'string' ||
430
+ manifest.photoLibrary.addOnly.trim() === '')
431
+ ) {
432
+ fail('photoLibrary.addOnly must be a non-empty string')
433
+ }
434
+ if (
435
+ manifest.photoLibrary?.readWrite !== undefined &&
436
+ (typeof manifest.photoLibrary.readWrite !== 'string' ||
437
+ manifest.photoLibrary.readWrite.trim() === '')
438
+ ) {
439
+ fail('photoLibrary.readWrite must be a non-empty string')
440
+ }
441
+ if (
442
+ manifest.contacts !== undefined &&
443
+ (!manifest.contacts ||
444
+ typeof manifest.contacts.usage !== 'string' ||
445
+ manifest.contacts.usage.trim() === '')
446
+ ) {
447
+ fail('contacts.usage must be a non-empty string')
448
+ }
449
+ if (manifest.calendar !== undefined) {
450
+ if (
451
+ !manifest.calendar ||
452
+ (manifest.calendar.usage === undefined &&
453
+ manifest.calendar.remindersUsage === undefined)
454
+ ) {
455
+ fail('calendar.usage or calendar.remindersUsage must be a non-empty string')
456
+ }
457
+ if (
458
+ manifest.calendar.usage !== undefined &&
459
+ (typeof manifest.calendar.usage !== 'string' ||
460
+ manifest.calendar.usage.trim() === '')
461
+ ) {
462
+ fail('calendar.usage must be a non-empty string')
463
+ }
464
+ if (
465
+ manifest.calendar.remindersUsage !== undefined &&
466
+ (typeof manifest.calendar.remindersUsage !== 'string' ||
467
+ manifest.calendar.remindersUsage.trim() === '')
468
+ ) {
469
+ fail('calendar.remindersUsage must be a non-empty string')
470
+ }
471
+ }
472
+ if (
473
+ manifest.location !== undefined &&
474
+ (!manifest.location ||
475
+ typeof manifest.location.whenInUse !== 'string' ||
476
+ manifest.location.whenInUse.trim() === '')
477
+ ) {
478
+ fail('location.whenInUse must be a non-empty string')
479
+ }
480
+ if (
481
+ manifest.location?.background !== undefined &&
482
+ typeof manifest.location.background !== 'boolean'
483
+ ) {
484
+ fail('location.background must be a boolean')
485
+ }
486
+ if (
487
+ manifest.location?.alwaysAndWhenInUse !== undefined &&
488
+ (typeof manifest.location.alwaysAndWhenInUse !== 'string' ||
489
+ manifest.location.alwaysAndWhenInUse.trim() === '')
490
+ ) {
491
+ fail('location.alwaysAndWhenInUse must be a non-empty string')
492
+ }
493
+ if (manifest.audio !== undefined) {
494
+ if (!manifest.audio || typeof manifest.audio !== 'object') {
495
+ fail('audio must configure microphone or background playback')
496
+ }
497
+ if (
498
+ manifest.audio.microphone !== undefined &&
499
+ (typeof manifest.audio.microphone !== 'string' ||
500
+ manifest.audio.microphone.trim() === '')
501
+ ) {
502
+ fail('audio.microphone must be a non-empty string')
503
+ }
504
+ if (
505
+ manifest.audio.background !== undefined &&
506
+ typeof manifest.audio.background !== 'boolean'
507
+ ) {
508
+ fail('audio.background must be a boolean')
509
+ }
510
+ if (manifest.audio.microphone === undefined && manifest.audio.background !== true) {
511
+ fail('audio must configure microphone or background playback')
512
+ }
513
+ }
514
+ if (
515
+ manifest.ios?.faceIdUsageDescription !== undefined &&
516
+ (typeof manifest.ios.faceIdUsageDescription !== 'string' ||
517
+ manifest.ios.faceIdUsageDescription.trim() === '')
518
+ ) {
519
+ fail('ios.faceIdUsageDescription must be a non-empty string')
520
+ }
521
+ if (
522
+ manifest.speech !== undefined &&
523
+ (typeof manifest.speech?.recognition !== 'string' ||
524
+ manifest.speech.recognition.trim() === '' ||
525
+ typeof manifest.speech.microphone !== 'string' ||
526
+ manifest.speech.microphone.trim() === '')
527
+ ) {
528
+ fail('speech.recognition and speech.microphone must be non-empty strings')
529
+ }
530
+ if (manifest.brightness !== undefined && typeof manifest.brightness !== 'boolean') {
531
+ fail('brightness must be a boolean')
532
+ }
533
+ if (
534
+ manifest.pedometer !== undefined &&
535
+ (typeof manifest.pedometer?.usage !== 'string' ||
536
+ manifest.pedometer.usage.trim() === '')
537
+ ) {
538
+ fail('pedometer.usage must be a non-empty string')
539
+ }
135
540
  if (manifest.notifications !== undefined) {
136
541
  if (
137
542
  typeof manifest.notifications !== 'object' ||
@@ -140,6 +545,21 @@ export function validateNativeApp(
140
545
  ) {
141
546
  fail('notifications.push must be a boolean')
142
547
  }
548
+ const aps = manifest.notifications.apsEnvironment
549
+ if (aps !== undefined && aps !== 'development' && aps !== 'production') {
550
+ fail(`notifications.apsEnvironment "${aps}" must be development or production`)
551
+ }
552
+ if (aps !== undefined && !manifest.notifications.push) {
553
+ fail('notifications.apsEnvironment needs notifications.push')
554
+ }
555
+ }
556
+ const accent = manifest.ios?.accentColor
557
+ if (
558
+ accent !== undefined &&
559
+ (!HEX_COLOR.test(accent.light) ||
560
+ (accent.dark !== undefined && !HEX_COLOR.test(accent.dark)))
561
+ ) {
562
+ fail('ios.accentColor light and dark must be six-digit hex colors')
143
563
  }
144
564
  if (
145
565
  manifest.pictureInPicture !== undefined &&
@@ -147,6 +567,20 @@ export function validateNativeApp(
147
567
  ) {
148
568
  fail('pictureInPicture must be a boolean')
149
569
  }
570
+ if (manifest.updates !== undefined) {
571
+ if (
572
+ typeof manifest.updates.runtimeVersion !== 'string' ||
573
+ manifest.updates.runtimeVersion.trim() === ''
574
+ ) {
575
+ fail('updates.runtimeVersion must be a non-empty string')
576
+ }
577
+ if (
578
+ manifest.updates.url !== undefined &&
579
+ (typeof manifest.updates.url !== 'string' || manifest.updates.url.trim() === '')
580
+ ) {
581
+ fail('updates.url must be a non-empty string')
582
+ }
583
+ }
150
584
  if (!platform || platform === 'ios') {
151
585
  if (!manifest.ios?.bundleId || !REVERSE_DNS.test(manifest.ios.bundleId)) {
152
586
  fail(`ios.bundleId "${manifest.ios?.bundleId}" must be reverse-dns`)
@@ -217,6 +651,34 @@ export function validateNativeApp(
217
651
  `android.versionCode "${manifest.android.versionCode}" must be a positive integer`
218
652
  )
219
653
  }
654
+ for (const key of ['targetSdk', 'compileSdk'] as const) {
655
+ const value = manifest.android[key]
656
+ if (value !== undefined && (!Number.isInteger(value) || value < 21)) {
657
+ fail(`android.${key} "${value}" must be an api level integer`)
658
+ }
659
+ }
660
+ if (manifest.android.shrinkResources && !manifest.android.minify) {
661
+ fail('android.shrinkResources needs android.minify')
662
+ }
663
+ for (const link of manifest.android.appLinks ?? []) {
664
+ if (!link.host || !REVERSE_DNS.test(link.host)) {
665
+ fail(`android.appLinks host "${link.host}" must be a domain`)
666
+ }
667
+ if (link.pathPrefix !== undefined && !link.pathPrefix.startsWith('/')) {
668
+ fail(`android.appLinks pathPrefix "${link.pathPrefix}" must start with /`)
669
+ }
670
+ }
671
+ const adaptiveIcon = manifest.android.adaptiveIcon
672
+ if (
673
+ adaptiveIcon !== undefined &&
674
+ (!adaptiveIcon.foreground ||
675
+ (adaptiveIcon.backgroundColor !== undefined &&
676
+ !HEX_COLOR.test(adaptiveIcon.backgroundColor)))
677
+ ) {
678
+ fail(
679
+ 'android.adaptiveIcon requires foreground, and backgroundColor must be six-digit hex'
680
+ )
681
+ }
220
682
  if (
221
683
  manifest.android.googleMapsApiKey !== undefined &&
222
684
  (typeof manifest.android.googleMapsApiKey !== 'string' ||
@@ -227,3 +689,57 @@ export function validateNativeApp(
227
689
  }
228
690
  return manifest
229
691
  }
692
+
693
+ // the expo config shape the dev server's manifest carries (`extra.expoClient`)
694
+ // for an app that declares native.app and no expo: clients that read an app's
695
+ // name, scheme or splash from the manifest read a one app the same way.
696
+ export function expoClientFromNativeApp(app: NativeAppManifest) {
697
+ return {
698
+ name: app.displayName ?? app.name,
699
+ slug: app.name,
700
+ scheme: app.scheme,
701
+ version: app.version,
702
+ orientation: app.orientation,
703
+ // an expo config without the key means light, while prebuild writes no
704
+ // UIUserInterfaceStyle for an unset style and the app follows the system,
705
+ // so the manifest states the style the built app has.
706
+ userInterfaceStyle: app.userInterfaceStyle ?? 'automatic',
707
+ icon: app.icon?.source,
708
+ splash: app.splash && {
709
+ image: app.splash.source,
710
+ backgroundImage: app.splash.backgroundImage,
711
+ backgroundColor: app.splash.backgroundColor,
712
+ imageWidth: app.splash.width,
713
+ resizeMode: app.splash.resizeMode,
714
+ dark: app.splash.dark && {
715
+ image: app.splash.dark.source,
716
+ backgroundImage: app.splash.dark.backgroundImage,
717
+ backgroundColor: app.splash.dark.backgroundColor,
718
+ },
719
+ },
720
+ plugins: app.fonts?.length ? [['expo-font', { fonts: app.fonts }]] : undefined,
721
+ ios: app.ios && {
722
+ bundleIdentifier: app.ios.bundleId,
723
+ buildNumber: app.ios.buildNumber,
724
+ supportsTablet: app.ios.tablet,
725
+ associatedDomains: app.ios.associatedDomains,
726
+ usesAppleSignIn: app.ios.usesAppleSignIn,
727
+ googleServicesFile: app.ios.googleServicesFile,
728
+ infoPlist: app.ios.infoPlist,
729
+ entitlements: app.ios.entitlements,
730
+ },
731
+ android: app.android && {
732
+ package: app.android.applicationId,
733
+ versionCode: app.android.versionCode,
734
+ permissions: app.android.permissions,
735
+ blockedPermissions: app.android.blockedPermissions,
736
+ googleServicesFile: app.android.googleServicesFile,
737
+ adaptiveIcon: app.android.adaptiveIcon && {
738
+ foregroundImage: app.android.adaptiveIcon.foreground,
739
+ backgroundImage: app.android.adaptiveIcon.background,
740
+ backgroundColor: app.android.adaptiveIcon.backgroundColor,
741
+ monochromeImage: app.android.adaptiveIcon.monochrome,
742
+ },
743
+ },
744
+ }
745
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"detectPackageManager.d.ts","sourceRoot":"","sources":["../src/detectPackageManager.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAA;AA+ChE,eAAO,MAAM,oBAAoB,aAAmB;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE;;;;;EAwBnE,CAAA"}
1
+ {"version":3,"file":"detectPackageManager.d.ts","sourceRoot":"","sources":["../src/detectPackageManager.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAA;AAgDhE,eAAO,MAAM,oBAAoB,aAAmB;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE;;;;;EAwBnE,CAAA"}