expo 58.0.0-preview.2 → 58.0.0-preview.4

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.
@@ -10,7 +10,7 @@ buildscript {
10
10
  }
11
11
 
12
12
  group = 'host.exp.exponent'
13
- version = '58.0.0-preview.2'
13
+ version = '58.0.0-preview.4'
14
14
 
15
15
  expoModule {
16
16
  // We can't prebuild the module because it depends on the generated files.
@@ -21,7 +21,7 @@ android {
21
21
  namespace "expo.core"
22
22
  defaultConfig {
23
23
  versionCode 1
24
- versionName "58.0.0-preview.2"
24
+ versionName "58.0.0-preview.4"
25
25
  consumerProguardFiles("proguard-rules.pro")
26
26
  }
27
27
  testOptions {
@@ -1 +1 @@
1
- export default function DevLoadingView(): import("react/jsx-runtime").JSX.Element | null;
1
+ export default function DevLoadingView(): import("react").JSX.Element | null;
@@ -1 +1 @@
1
- export default function DevLoadingView(): import("react/jsx-runtime").JSX.Element | null;
1
+ export default function DevLoadingView(): import("react").JSX.Element | null;
@@ -1 +1 @@
1
- export declare function AppEntryNotFound(): import("react/jsx-runtime").JSX.Element;
1
+ export declare function AppEntryNotFound(): import("react").JSX.Element;
@@ -1,9 +1,9 @@
1
1
  {
2
- "@expo/dom-webview": "~58.0.0",
2
+ "@expo/dom-webview": "~58.0.1",
3
3
  "@expo/fingerprint": "~0.21.1",
4
- "@expo/metro-runtime": "~58.0.2",
4
+ "@expo/metro-runtime": "~58.0.4",
5
5
  "@expo/vector-icons": "^15.0.2",
6
- "@expo/ui": "~58.0.2",
6
+ "@expo/ui": "~58.0.4",
7
7
  "@react-native-async-storage/async-storage": "2.2.0",
8
8
  "@react-native-community/datetimepicker": "9.2.1",
9
9
  "@react-native-masked-view/masked-view": "0.3.2",
@@ -13,95 +13,95 @@
13
13
  "@react-native-picker/picker": "2.11.4",
14
14
  "@react-native-segmented-control/segmented-control": "2.5.7",
15
15
  "@stripe/stripe-react-native": "0.76.0",
16
- "eslint-config-expo": "~58.0.1",
16
+ "eslint-config-expo": "~58.0.2",
17
17
  "expo-analytics-amplitude": "~11.3.0",
18
18
  "expo-app-auth": "~11.1.0",
19
- "expo-app-intents": "~0.4.0",
19
+ "expo-app-intents": "~0.4.2",
20
20
  "expo-app-loader-provider": "~8.0.0",
21
- "expo-app-metrics": "~58.0.2",
22
- "expo-apple-authentication": "~58.0.0",
21
+ "expo-app-metrics": "~58.0.4",
22
+ "expo-apple-authentication": "~58.0.1",
23
23
  "expo-application": "~58.0.1",
24
- "expo-asset": "~58.0.2",
25
- "expo-audio": "~58.0.0",
26
- "expo-auth-session": "~58.0.1",
27
- "expo-background-fetch": "~58.0.2",
28
- "expo-background-task": "~58.0.2",
29
- "expo-battery": "~58.0.0",
30
- "expo-blur": "~58.0.0",
24
+ "expo-asset": "~58.0.4",
25
+ "expo-audio": "~58.0.1",
26
+ "expo-auth-session": "~58.0.3",
27
+ "expo-background-fetch": "~58.0.4",
28
+ "expo-background-task": "~58.0.4",
29
+ "expo-battery": "~58.0.1",
30
+ "expo-blur": "~58.0.1",
31
31
  "expo-brightness": "~58.0.0",
32
- "expo-brownfield": "~58.0.2",
33
- "expo-build-properties": "~58.0.2",
34
- "expo-calendar": "~58.0.1",
35
- "expo-camera": "~58.0.1",
32
+ "expo-brownfield": "~58.0.4",
33
+ "expo-build-properties": "~58.0.4",
34
+ "expo-calendar": "~58.0.2",
35
+ "expo-camera": "~58.0.2",
36
36
  "expo-cellular": "~58.0.0",
37
- "expo-checkbox": "~58.0.0",
38
- "expo-clipboard": "~58.0.0",
39
- "expo-constants": "~58.0.2",
40
- "expo-contacts": "~58.0.1",
41
- "expo-crypto": "~58.0.1",
42
- "expo-dev-client": "~58.0.2",
43
- "expo-device": "~58.0.0",
37
+ "expo-checkbox": "~58.0.1",
38
+ "expo-clipboard": "~58.0.1",
39
+ "expo-constants": "~58.0.4",
40
+ "expo-contacts": "~58.0.2",
41
+ "expo-crypto": "~58.0.2",
42
+ "expo-dev-client": "~58.0.4",
43
+ "expo-device": "~58.0.1",
44
44
  "expo-document-picker": "~58.0.0",
45
45
  "expo-eas-client": "~58.0.0",
46
46
  "expo-file-system": "~58.0.0",
47
- "expo-font": "~58.0.1",
48
- "expo-gl": "~58.0.0",
49
- "expo-glass-effect": "~58.0.0",
47
+ "expo-font": "~58.0.2",
48
+ "expo-gl": "~58.0.1",
49
+ "expo-glass-effect": "~58.0.1",
50
50
  "expo-google-app-auth": "~8.3.0",
51
51
  "expo-haptics": "~58.0.1",
52
- "expo-image": "~58.0.2",
52
+ "expo-image": "~58.0.3",
53
53
  "expo-image-loader": "~58.0.1",
54
- "expo-image-manipulator": "~58.0.3",
55
- "expo-image-picker": "~58.0.2",
54
+ "expo-image-manipulator": "~58.0.5",
55
+ "expo-image-picker": "~58.0.4",
56
56
  "expo-intent-launcher": "~58.0.0",
57
- "expo-insights": "~58.0.2",
58
- "expo-keep-awake": "~58.0.0",
59
- "expo-linear-gradient": "~58.0.0",
60
- "expo-linking": "~58.0.2",
57
+ "expo-insights": "~58.0.4",
58
+ "expo-keep-awake": "~58.0.1",
59
+ "expo-linear-gradient": "~58.0.1",
60
+ "expo-linking": "~58.0.4",
61
61
  "expo-local-authentication": "~58.0.0",
62
- "expo-localization": "~58.0.0",
63
- "expo-location": "~58.0.2",
62
+ "expo-localization": "~58.0.1",
63
+ "expo-location": "~58.0.4",
64
64
  "expo-mail-composer": "~58.0.0",
65
65
  "expo-manifests": "~58.0.0",
66
- "expo-maps": "~58.0.1",
66
+ "expo-maps": "~58.0.2",
67
67
  "expo-mcp": "~0.2.1",
68
68
  "expo-media-library": "~58.0.0",
69
- "expo-mesh-gradient": "~58.0.0",
70
- "expo-module-template": "~58.0.1",
71
- "expo-modules-core": "~58.0.2",
72
- "expo-navigation-bar": "~58.0.0",
73
- "expo-network": "~58.0.0",
74
- "expo-notifications": "~58.0.2",
75
- "expo-observe": "~58.0.4",
76
- "expo-print": "~58.0.0",
77
- "expo-live-photo": "~58.0.0",
78
- "expo-router": "~58.0.3",
79
- "expo-screen-capture": "~58.0.0",
69
+ "expo-mesh-gradient": "~58.0.1",
70
+ "expo-module-template": "~58.0.2",
71
+ "expo-modules-core": "~58.0.4",
72
+ "expo-navigation-bar": "~58.0.1",
73
+ "expo-network": "~58.0.1",
74
+ "expo-notifications": "~58.0.4",
75
+ "expo-observe": "~58.0.6",
76
+ "expo-print": "~58.0.1",
77
+ "expo-live-photo": "~58.0.1",
78
+ "expo-router": "~58.0.5",
79
+ "expo-screen-capture": "~58.0.1",
80
80
  "expo-screen-orientation": "~58.0.0",
81
81
  "expo-secure-store": "~58.0.0",
82
82
  "expo-sensors": "~58.0.0",
83
- "expo-server": "~58.0.0",
84
- "expo-sharing": "~58.0.4",
83
+ "expo-server": "~58.0.1",
84
+ "expo-sharing": "~58.0.7",
85
85
  "expo-sms": "~58.0.0",
86
86
  "expo-speech": "~58.0.0",
87
- "expo-splash-screen": "~58.0.0",
88
- "expo-sqlite": "~58.0.3",
89
- "expo-status-bar": "~58.0.0",
87
+ "expo-splash-screen": "~58.0.1",
88
+ "expo-sqlite": "~58.0.4",
89
+ "expo-status-bar": "~58.0.1",
90
90
  "expo-store-review": "~58.0.0",
91
- "expo-symbols": "~58.0.1",
92
- "expo-system-ui": "~58.0.1",
93
- "expo-task-manager": "~58.0.3",
91
+ "expo-symbols": "~58.0.2",
92
+ "expo-system-ui": "~58.0.2",
93
+ "expo-task-manager": "~58.0.5",
94
94
  "expo-tracking-transparency": "~58.0.0",
95
- "expo-updates": "~58.0.4",
95
+ "expo-updates": "~58.0.6",
96
96
  "expo-video-thumbnails": "~58.0.0",
97
- "expo-video": "~58.0.1",
98
- "expo-web-browser": "~58.0.0",
99
- "expo-widgets": "~58.0.2",
100
- "jest-expo": "~58.0.2",
97
+ "expo-video": "~58.0.2",
98
+ "expo-web-browser": "~58.0.1",
99
+ "expo-widgets": "~58.0.4",
100
+ "jest-expo": "~58.0.3",
101
101
  "lottie-react-native": "~7.5.0",
102
- "react": "19.2.3",
103
- "react-dom": "19.2.3",
104
- "react-native": "0.88.0-rc.0",
102
+ "react": "19.3.0",
103
+ "react-dom": "19.3.0",
104
+ "react-native": "0.88.0-rc.1",
105
105
  "react-native-web": "~0.21.0",
106
106
  "react-native-gesture-handler": "~3.2.1",
107
107
  "react-native-keyboard-controller": "1.22.4",
@@ -109,12 +109,12 @@
109
109
  "react-native-pager-view": "9.0.4",
110
110
  "react-native-worklets": "0.12.2",
111
111
  "react-native-reanimated": "4.6.0",
112
- "react-native-screens": "~4.27.0",
112
+ "react-native-screens": "~4.28.0",
113
113
  "react-native-safe-area-context": "~5.9.1",
114
114
  "react-native-svg": "15.15.5",
115
115
  "react-native-view-shot": "5.1.1",
116
116
  "react-native-webview": "14.0.1",
117
- "react-server-dom-webpack": "~19.2.4",
117
+ "react-server-dom-webpack": "~19.3.0",
118
118
  "sentry-expo": "~7.0.0",
119
119
  "unimodules-app-loader": "~58.0.0",
120
120
  "unimodules-image-loader-interface": "~6.1.0",
@@ -104,7 +104,17 @@ open class ExpoAppSceneDelegate: UIResponder, UIWindowSceneDelegate {
104
104
  }
105
105
 
106
106
  open func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
107
- URLContexts.forEach { forwarder.open(url: $0.url, options: Self.openURLOptions(from: $0.options)) }
107
+ // React Native ignores the app-delegate URL API under the scene life cycle, so a warm link only
108
+ // reaches JS through the scene API. It reads a single context, hence one call per context.
109
+ URLContexts.forEach { context in
110
+ forwarder.open(url: context.url, options: Self.openURLOptions(from: context.options)) {
111
+ // TODO: Remove this when bumping react-native-tvos to 0.88
112
+ let selector = NSSelectorFromString("scene:openURLContexts:")
113
+ if RCTLinkingManager.responds(to: selector) {
114
+ _ = RCTLinkingManager.perform(selector, with: scene, with: Set([context]))
115
+ }
116
+ }
117
+ }
108
118
  }
109
119
 
110
120
  open func scene(_ scene: UIScene, willContinueUserActivityWithType userActivityType: String) {
@@ -112,7 +122,12 @@ open class ExpoAppSceneDelegate: UIResponder, UIWindowSceneDelegate {
112
122
  }
113
123
 
114
124
  open func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
115
- forwarder.continue(userActivity)
125
+ forwarder.continue(userActivity) {
126
+ let selector = NSSelectorFromString("scene:continueUserActivity:")
127
+ if RCTLinkingManager.responds(to: selector) {
128
+ _ = RCTLinkingManager.perform(selector, with: scene, with: userActivity)
129
+ }
130
+ }
116
131
  }
117
132
 
118
133
  open func scene(
@@ -1,5 +1,7 @@
1
1
  // Copyright 2015-present 650 Industries. All rights reserved.
2
2
 
3
+ import Foundation
4
+
3
5
  /**
4
6
  A data structure to store response body chunks
5
7
  */
@@ -1,5 +1,6 @@
1
1
  // Copyright 2015-present 650 Industries. All rights reserved.
2
2
 
3
+ import ExpoModulesCore
3
4
  import Foundation
4
5
  import React
5
6
 
@@ -19,18 +20,33 @@ import React
19
20
  struct SceneEventForwarder {
20
21
  var appDelegate: () -> ExpoAppDelegate? = { UIApplication.shared.delegate as? ExpoAppDelegate }
21
22
 
22
- func open(url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) {
23
+ func open(
24
+ url: URL,
25
+ options: [UIApplication.OpenURLOptionsKey: Any],
26
+ notifyReactNative: (() -> Void)? = nil
27
+ ) {
23
28
  let application = UIApplication.shared
24
29
  let delegate = appDelegate()
25
30
 
31
+ #if DEBUG
32
+ if EmbeddedFingerprint.CheckProtocol.isCheckURL(url) {
33
+ _ = delegate?.application(application, open: url, options: options)
34
+ return
35
+ }
36
+ #endif
37
+
26
38
  notifyLinkingManagerUnlessAlreadyNotified(of: url) {
27
39
  _ = delegate?.application(application, open: url, options: options)
28
40
  } notify: {
29
- RCTLinkingManager.application(application, open: url, options: options)
41
+ if let notifyReactNative {
42
+ notifyReactNative()
43
+ } else {
44
+ RCTLinkingManager.application(application, open: url, options: options)
45
+ }
30
46
  }
31
47
  }
32
48
 
33
- func `continue`(_ userActivity: NSUserActivity) {
49
+ func `continue`(_ userActivity: NSUserActivity, notifyReactNative: (() -> Void)? = nil) {
34
50
  let application = UIApplication.shared
35
51
  let delegate = appDelegate()
36
52
 
@@ -39,7 +55,11 @@ struct SceneEventForwarder {
39
55
  notifyLinkingManagerUnlessAlreadyNotified(of: userActivity.webpageURL) {
40
56
  _ = delegate?.application(application, continue: userActivity, restorationHandler: { _ in })
41
57
  } notify: {
42
- RCTLinkingManager.application(application, continue: userActivity, restorationHandler: { _ in })
58
+ if let notifyReactNative {
59
+ notifyReactNative()
60
+ } else {
61
+ RCTLinkingManager.application(application, continue: userActivity, restorationHandler: { _ in })
62
+ }
43
63
  }
44
64
  }
45
65
 
@@ -220,6 +220,25 @@ struct ExpoAppSceneDelegateTests {
220
220
  #expect(recorder.count(of: url) == 1)
221
221
  }
222
222
 
223
+ @Test
224
+ @MainActor
225
+ func `keeps a fingerprint-check trigger away from RCTLinkingManager so JS does not see it`() {
226
+ let url = URL(string: "bareexpo://?__expo_fingerprint_check=1&__expo_fingerprint_nonce=abc")!
227
+ let recorder = OpenURLNotificationRecorder()
228
+ let spy = SpyAppDelegate()
229
+ SceneEventForwarder(appDelegate: { spy }).open(url: url, options: [:])
230
+ #expect(recorder.count(of: url) == 0)
231
+ }
232
+
233
+ @Test
234
+ @MainActor
235
+ func `still hands a fingerprint-check trigger to the app delegate`() {
236
+ let url = URL(string: "bareexpo://?__expo_fingerprint_check=1&__expo_fingerprint_nonce=def")!
237
+ let delegate = LegacyLinkingAppDelegate()
238
+ SceneEventForwarder(appDelegate: { delegate }).open(url: url, options: [:])
239
+ #expect(delegate.openedURLs == [url])
240
+ }
241
+
223
242
  @Test
224
243
  @MainActor
225
244
  func `notifies RCTLinkingManager once when the delegate notifies it too`() {
@@ -244,6 +263,51 @@ struct ExpoAppSceneDelegateTests {
244
263
  #expect(recorder.count(of: webpageURL) == 1)
245
264
  }
246
265
 
266
+ @Test
267
+ @MainActor
268
+ func `notifies React Native through the injected notifier for a warm URL`() {
269
+ let spy = SpyAppDelegate()
270
+ let url = URL(string: "bareexpo://scene-delegate/warm-open-url")!
271
+ let recorder = OpenURLNotificationRecorder()
272
+ var notifications = 0
273
+ SceneEventForwarder(appDelegate: { spy }).open(url: url, options: [:]) {
274
+ notifications += 1
275
+ }
276
+ #expect(spy.openedURLs.first?.url == url)
277
+ #expect(notifications == 1)
278
+ #expect(recorder.count(of: url) == 0)
279
+ }
280
+
281
+ @Test
282
+ @MainActor
283
+ func `does not notify React Native when the app delegate already notified for a URL`() {
284
+ let delegate = LegacyLinkingAppDelegate()
285
+ let url = URL(string: "bareexpo://scene-delegate/warm-legacy-open-url")!
286
+ var notifications = 0
287
+ SceneEventForwarder(appDelegate: { delegate }).open(url: url, options: [:]) {
288
+ notifications += 1
289
+ }
290
+ #expect(delegate.openedURLs == [url])
291
+ #expect(notifications == 0)
292
+ }
293
+
294
+ @Test
295
+ @MainActor
296
+ func `notifies React Native through the injected notifier for a warm user activity`() {
297
+ let spy = SpyAppDelegate()
298
+ let userActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb)
299
+ let webpageURL = URL(string: "https://expo.dev/scene-delegate/warm-activity")!
300
+ userActivity.webpageURL = webpageURL
301
+ let recorder = OpenURLNotificationRecorder()
302
+ var notifications = 0
303
+ SceneEventForwarder(appDelegate: { spy }).continue(userActivity) {
304
+ notifications += 1
305
+ }
306
+ #expect(spy.continuedUserActivities.first === userActivity)
307
+ #expect(notifications == 1)
308
+ #expect(recorder.count(of: webpageURL) == 0)
309
+ }
310
+
247
311
  @Test
248
312
  @MainActor
249
313
  func `does not notify subscribers without an ExpoAppDelegate`() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo",
3
- "version": "58.0.0-preview.2",
3
+ "version": "58.0.0-preview.4",
4
4
  "description": "The Expo SDK",
5
5
  "keywords": [
6
6
  "expo"
@@ -23,6 +23,8 @@
23
23
  },
24
24
  "files": [
25
25
  "android",
26
+ "!android/.gitignore",
27
+ "!android/build/**",
26
28
  "bin",
27
29
  "build",
28
30
  "internal",
@@ -198,40 +200,40 @@
198
200
  },
199
201
  "dependencies": {
200
202
  "@babel/runtime": "^7.20.0",
201
- "@expo/cli": "^58.0.2",
203
+ "@expo/cli": "^58.0.4",
202
204
  "@expo/config": "~58.0.0",
203
- "@expo/config-plugins": "~58.0.1",
204
- "@expo/devtools": "~58.0.0",
205
- "@expo/dom-webview": "~58.0.0",
205
+ "@expo/config-plugins": "~58.0.2",
206
+ "@expo/devtools": "~58.0.1",
207
+ "@expo/dom-webview": "~58.0.1",
206
208
  "@expo/fingerprint": "^0.21.1",
207
209
  "@expo/local-build-cache-provider": "^58.0.0",
208
- "@expo/log-box": "^58.0.2",
210
+ "@expo/log-box": "^58.0.4",
209
211
  "@expo/log-box-utils": "^58.0.0",
210
212
  "@expo/metro": "~58.0.0-rc.0",
211
- "@expo/metro-config": "~58.0.2",
213
+ "@expo/metro-config": "~58.0.4",
212
214
  "@ungap/structured-clone": "^1.3.0",
213
- "babel-preset-expo": "~58.0.2",
214
- "expo-asset": "~58.0.2",
215
- "expo-constants": "~58.0.2",
215
+ "babel-preset-expo": "~58.0.4",
216
+ "expo-asset": "~58.0.4",
217
+ "expo-constants": "~58.0.4",
216
218
  "expo-file-system": "~58.0.0",
217
- "expo-font": "~58.0.1",
218
- "expo-keep-awake": "~58.0.0",
219
- "expo-modules-autolinking": "~58.0.2",
220
- "expo-modules-core": "~58.0.2",
219
+ "expo-font": "~58.0.2",
220
+ "expo-keep-awake": "~58.0.1",
221
+ "expo-modules-autolinking": "~58.0.3",
222
+ "expo-modules-core": "~58.0.4",
221
223
  "pretty-format": "^29.7.0",
222
224
  "react-refresh": "^0.14.2",
223
225
  "whatwg-url-minimum": "^0.2.0"
224
226
  },
225
227
  "devDependencies": {
226
- "@expo/dom-webview": "58.0.0",
227
- "@expo/metro-runtime": "58.0.2",
228
+ "@expo/dom-webview": "58.0.1",
229
+ "@expo/metro-runtime": "58.0.4",
228
230
  "@types/node": "^22.14.0",
229
- "@types/react": "~19.2.0",
230
- "expo-updates": "58.0.4",
231
+ "@types/react": "~19.3.0",
232
+ "expo-updates": "58.0.6",
231
233
  "npm-run-all2": "^8.0.4",
232
- "react": "19.2.3",
233
- "react-dom": "19.2.3",
234
- "react-native": "0.88.0-rc.0",
234
+ "react": "19.3.0",
235
+ "react-dom": "19.3.0",
236
+ "react-native": "0.88.0-rc.1",
235
237
  "rimraf": "^6.1.2",
236
238
  "web-streams-polyfill": "^3.3.2"
237
239
  },
@@ -267,13 +269,14 @@
267
269
  "engines": {
268
270
  "node": "^22.13.0 || ^24.3.0 || ^26.0.0 || >=27.0.0"
269
271
  },
270
- "gitHead": "313e67f5c9dfb16f9deca74a271cd1d4f33a6f82",
272
+ "gitHead": "ebb1fd7569c9c9e172cb9abd73dfede027c1d674",
271
273
  "scripts": {
272
274
  "build": "../expo-module-scripts/bin/expo-build src",
273
275
  "clean": "rimraf build",
274
276
  "lint": "oxlint --config oxlint.config.mjs .",
275
277
  "format": "expo-module format",
276
- "test": "jest",
278
+ "test": "jest && jest --config scripts/spm/jest.config.js",
279
+ "test:spm-plugin": "jest --config scripts/spm/jest.config.js",
277
280
  "depscheck": "expo-module depscheck",
278
281
  "typecheck": "tsc -p tsconfig.json"
279
282
  }
@@ -47,4 +47,11 @@ module.exports = {
47
47
  windows: null,
48
48
  },
49
49
  },
50
+ // React Native SwiftPM autolinking plugin (preview). Discovered transitively by
51
+ // `react-native spm` when `expo` is an autolinked dependency; contributes Expo's
52
+ // native modules (precompiled xcframeworks + source packages) and the generated
53
+ // ExpoModulesProvider into the SwiftPM autolinking graph.
54
+ spm: {
55
+ autolinkingPlugin: './scripts/spm/plugin.js',
56
+ },
50
57
  };
@@ -0,0 +1,118 @@
1
+ # Expo SwiftPM autolinking plugin
2
+
3
+ This preview plugin is discovered through `expo/react-native.config.js` by
4
+ React Native's `react-native spm` tooling. It contributes Expo source packages,
5
+ the generated `ExpoModulesProvider.swift`, and precompiled Expo frameworks.
6
+
7
+ ## Precompiled framework contract
8
+
9
+ Every precompiled Expo framework is returned as one immutable pair:
10
+
11
+ ```js
12
+ {
13
+ id: 'expo-modules-core',
14
+ frameworkName: 'ExpoModulesCore',
15
+ linkage: 'dynamic',
16
+ flavors: {
17
+ debug: '/absolute/path/ExpoModulesCore.xcframework',
18
+ release: '/absolute/path/ExpoModulesCore.xcframework',
19
+ },
20
+ }
21
+ ```
22
+
23
+ Both flavors are mandatory. A partial build, relative path, wrong XCFramework
24
+ name, missing `Info.plist`, or non-dynamic declaration aborts autolinking. The
25
+ plugin does not create or mutate a “current flavor” symlink.
26
+
27
+ Artifact lookup is deterministic:
28
+
29
+ 1. `EXPO_PRECOMPILED_MODULES_PATH/<package>/output/<flavor>/xcframeworks`
30
+ 2. `packages/precompile/.build/<package>/output/<flavor>/xcframeworks`
31
+ 3. `<installed-package>/prebuilds/output/<flavor>/xcframeworks`
32
+
33
+ An artifact may be an expanded `.xcframework` or a bundled `.tar.gz`. Bundled
34
+ tarballs are expanded into `ios/build/expo-xcframeworks` before the plugin
35
+ returns, so `spm add` and `spm update` always receive both real paths. Expo's
36
+ prebuild command builds Debug and Release by default; do not use a single-flavor
37
+ prebuild for an app that consumes the SwiftPM plugin.
38
+
39
+ Lookup runs for **every** resolved pod, not just the core frameworks. Any module
40
+ with a `spm.config.json` product is consumed as a precompiled framework the
41
+ moment its artifact exists — no `Package.swift` is generated for it, and none is
42
+ needed. Path 3 is the published-package case: a module shipping
43
+ `prebuilds/output/<flavor>/xcframeworks` is picked up with no local build.
44
+
45
+ React Native normalizes the declared pairs into immutable app-local Debug and
46
+ Release slots. Xcode then selects, exact-links, embeds, and signs the right slot
47
+ for each configuration. Configurations containing `debug` or `development` use
48
+ Debug; all others use Release. Adding, removing, or changing a flavored Expo
49
+ framework requires `npx react-native spm update`; build-time `spm sync` only
50
+ refreshes invariant source/codegen output.
51
+
52
+ ## SwiftPM graph
53
+
54
+ Flavored runtime frameworks are not SwiftPM products or binary targets. The
55
+ plugin creates a binary-free compile interface tree containing their public
56
+ headers, module maps, and Swift module interfaces. Expo source packages compile
57
+ against that stable tree, while React Native owns the only runtime link/embed
58
+ path.
59
+
60
+ Every Expo source target uses exactly the products supplied in
61
+ `context.react.products`: `ReactHeaders`, `ReactNativeHeaders`,
62
+ `ReactNativeDependenciesHeaders`, and `ReactAppHeaders` when available. The
63
+ ExpoModulesCore module map itself declares `use React`, so these invariant
64
+ carriers are needed even when a module does not import React directly. The
65
+ plugin does not infer runtime React or Hermes products.
66
+
67
+ ## Unsupported modules
68
+
69
+ A module the plugin cannot contribute aborts autolinking. Dropping it would not
70
+ fail the build — it would fail at runtime with `Cannot find native module`, far
71
+ from the cause. Each one is reported as an `error:`-prefixed block, which Xcode
72
+ surfaces as a build error, naming the fix:
73
+
74
+ - **A prebuildable product with no built artifact** — the module declares a
75
+ product in its `spm.config.json`, so it needs no `Package.swift`; the artifact
76
+ is simply missing. `et prebuild <package>` (both flavors, so omit `--flavor`)
77
+ fixes it. This is the preferred route for a mixed-language module: the prebuild
78
+ pipeline compiles Swift, Objective-C, and C++ targets into one XCFramework, so
79
+ no source split is required.
80
+ - **Mixed Swift and Objective-C/C++ sources with neither manifest** — SwiftPM
81
+ compiles the two languages in separate targets, and only the module can
82
+ declare where its split goes. Either an `spm.config.json`
83
+ (`packages/expo-sensors` is a worked example) or a checked-in `Package.swift`
84
+ (`packages/expo-constants`) makes it consumable, persisted with
85
+ `patch-package` for a module you do not own, and upstreamed so every consumer
86
+ gets it. This mirrors what React Native's autolinker asks of community
87
+ libraries.
88
+ - **No `ios` or `apple` source directory** — an incomplete install, or a
89
+ non-standard layout that needs a `Package.swift` naming the real path.
90
+ - **Manifest targets whose sources are not on disk** — a checked-in
91
+ `Package.swift` whose targets declare no `path:` and have no sources under a
92
+ predefined source directory, so the generated targets would point at nothing.
93
+ - **Native linkage declared only in the podspec** — `s.frameworks`,
94
+ `s.weak_frameworks` or `s.libraries`. The plugin never reads linkage out of
95
+ Ruby: a misread link line fails in a shipped app, not here. Declare it as
96
+ `linkerSettings` in a `Package.swift`, which the plugin mirrors verbatim, or
97
+ ship an `spm.config.json` and prebuild the module.
98
+ - **An iOS deployment floor that is not a literal** — the plugin reads
99
+ `s.platforms = { :ios => '16.4' }` and `s.ios.deployment_target = '16.4'` as
100
+ text; anything computed would need the podspec to be run.
101
+ - **No precompiled ExpoModulesCore** — a single project-level fault reported
102
+ once, not per module, since every source module compiles against its interface
103
+ tree.
104
+
105
+ One podspec finding is a warning rather than an error: linker flags set through
106
+ `OTHER_LDFLAGS` in a `pod_target_xcconfig`. The module is still emitted, because
107
+ Swift links the system frameworks and C++ runtime its sources import; if it does
108
+ fail to link, declare the missing library as `linkerSettings` in a
109
+ `Package.swift`.
110
+
111
+ To build without a module, exclude it from autolinking in the app's
112
+ `package.json` (`expo.autolinking.exclude`); its native module is then
113
+ unavailable at runtime.
114
+
115
+ Emitted modules are also scanned for CocoaPods dependencies with no SwiftPM
116
+ counterpart (`SDWebImage`, `ZXingObjC`, …). Those are a warning, not an error:
117
+ the module enters the graph and compiles until a source file reaches the missing
118
+ dependency.