expo 56.0.8 → 56.0.10

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 = '56.0.8'
13
+ version = '56.0.10'
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 "56.0.8"
24
+ versionName "56.0.10"
25
25
  consumerProguardFiles("proguard-rules.pro")
26
26
  }
27
27
  testOptions {
@@ -6,9 +6,11 @@ import expo.modules.kotlin.AppContext
6
6
  import expo.modules.kotlin.sharedobjects.SharedObject
7
7
  import okhttp3.Call
8
8
  import okhttp3.CookieJar
9
+ import okhttp3.MediaType
9
10
  import okhttp3.MediaType.Companion.toMediaTypeOrNull
10
11
  import okhttp3.OkHttpClient
11
12
  import okhttp3.Request
13
+ import okhttp3.RequestBody
12
14
  import okhttp3.RequestBody.Companion.toRequestBody
13
15
  import java.net.URL
14
16
 
@@ -16,6 +18,19 @@ private data class RequestHolder(var request: Request?)
16
18
 
17
19
  internal val METHODS_REQUIRING_BODY = arrayOf("POST", "PUT", "PATCH")
18
20
 
21
+ internal fun buildRequestBody(method: String, requestBody: ByteArray?, mediaType: MediaType?): RequestBody? {
22
+ return requestBody?.toRequestBody(mediaType) ?: run {
23
+ // OkHttp requires a non-null body for POST/PATCH/PUT, unlike WinterTC fetch. Provide an empty
24
+ // (zero-length) body to match it, not `byteArrayOf(0)`, which sends a single 0x00 byte.
25
+ // Ref: https://github.com/expo/expo/issues/46668
26
+ if (method in METHODS_REQUIRING_BODY) {
27
+ ByteArray(0).toRequestBody(mediaType)
28
+ } else {
29
+ null
30
+ }
31
+ }
32
+ }
33
+
19
34
  internal class NativeRequest(appContext: AppContext, internal val response: NativeResponse) :
20
35
  SharedObject(appContext) {
21
36
  private val requestHolder = RequestHolder(null)
@@ -36,17 +51,7 @@ internal class NativeRequest(appContext: AppContext, internal val response: Nati
36
51
 
37
52
  val headers = requestInit.headers.toHeaders()
38
53
  val mediaType = headers["Content-Type"]?.toMediaTypeOrNull()
39
- val reqBody = requestBody?.toRequestBody(mediaType) ?: run {
40
- // OkHttp requires a non-null body for POST, PATCH, and PUT requests.
41
- // WinterTC fetch, however, does not have this limitation.
42
- // Provide an empty body to make OkHttp behave like WinterTC fetch.
43
- // Ref: https://github.com/expo/expo/issues/35950#issuecomment-3245173248
44
- if (requestInit.method in METHODS_REQUIRING_BODY) {
45
- byteArrayOf(0).toRequestBody(mediaType)
46
- } else {
47
- null
48
- }
49
- }
54
+ val reqBody = buildRequestBody(requestInit.method, requestBody, mediaType)
50
55
  val request = Request.Builder()
51
56
  .headers(headers)
52
57
  .method(requestInit.method, reqBody)
@@ -0,0 +1,33 @@
1
+ // Copyright 2015-present 650 Industries. All rights reserved.
2
+
3
+ package expo.modules.fetch
4
+
5
+ import com.google.common.truth.Truth.assertThat
6
+ import okhttp3.MediaType.Companion.toMediaTypeOrNull
7
+ import org.junit.Test
8
+
9
+ class NativeRequestTest {
10
+ private val jsonMediaType = "application/json".toMediaTypeOrNull()
11
+
12
+ @Test
13
+ fun `null body on a method requiring a body produces an empty body, not a null byte`() {
14
+ val body = buildRequestBody("POST", null, jsonMediaType)
15
+ assertThat(body).isNotNull()
16
+ // Regression test for https://github.com/expo/expo/issues/46668:
17
+ // OkHttp requires a non-null body for POST/PUT/PATCH, but the body must be
18
+ // empty (Content-Length: 0) rather than a single 0x00 byte (Content-Length: 1).
19
+ assertThat(body!!.contentLength()).isEqualTo(0L)
20
+ }
21
+
22
+ @Test
23
+ fun `null body on a method not requiring a body produces no body`() {
24
+ assertThat(buildRequestBody("GET", null, jsonMediaType)).isNull()
25
+ }
26
+
27
+ @Test
28
+ fun `a provided body is preserved`() {
29
+ val body = buildRequestBody("POST", "hello".toByteArray(), jsonMediaType)
30
+ assertThat(body).isNotNull()
31
+ assertThat(body!!.contentLength()).isEqualTo(5L)
32
+ }
33
+ }
@@ -1,8 +1,8 @@
1
1
  {
2
- "@expo/fingerprint": "~0.19.3",
3
- "@expo/metro-runtime": "~56.0.13",
2
+ "@expo/fingerprint": "~0.19.4",
3
+ "@expo/metro-runtime": "~56.0.15",
4
4
  "@expo/vector-icons": "^15.0.2",
5
- "@expo/ui": "~56.0.15",
5
+ "@expo/ui": "~56.0.17",
6
6
  "@react-native-async-storage/async-storage": "2.2.0",
7
7
  "@react-native-community/datetimepicker": "9.1.0",
8
8
  "@react-native-masked-view/masked-view": "0.3.2",
@@ -16,28 +16,28 @@
16
16
  "expo-analytics-amplitude": "~11.3.0",
17
17
  "expo-app-auth": "~11.1.0",
18
18
  "expo-app-loader-provider": "~8.0.0",
19
- "expo-app-metrics": "~56.0.16",
19
+ "expo-app-metrics": "~56.0.18",
20
20
  "expo-apple-authentication": "~56.0.4",
21
21
  "expo-application": "~56.0.3",
22
- "expo-asset": "~56.0.15",
23
- "expo-audio": "~56.0.11",
24
- "expo-auth-session": "~56.0.13",
25
- "expo-background-fetch": "~56.0.16",
26
- "expo-background-task": "~56.0.16",
22
+ "expo-asset": "~56.0.17",
23
+ "expo-audio": "~56.0.12",
24
+ "expo-auth-session": "~56.0.14",
25
+ "expo-background-fetch": "~56.0.18",
26
+ "expo-background-task": "~56.0.18",
27
27
  "expo-battery": "~56.0.4",
28
28
  "expo-blur": "~56.0.3",
29
29
  "expo-brightness": "~56.0.5",
30
- "expo-brownfield": "~56.0.17",
31
- "expo-build-properties": "~56.0.16",
30
+ "expo-brownfield": "~56.0.19",
31
+ "expo-build-properties": "~56.0.18",
32
32
  "expo-calendar": "~56.0.8",
33
33
  "expo-camera": "~56.0.7",
34
34
  "expo-cellular": "~56.0.5",
35
35
  "expo-checkbox": "~56.0.1",
36
- "expo-clipboard": "~56.0.3",
37
- "expo-constants": "~56.0.16",
38
- "expo-contacts": "~56.0.7",
36
+ "expo-clipboard": "~56.0.4",
37
+ "expo-constants": "~56.0.18",
38
+ "expo-contacts": "~56.0.8",
39
39
  "expo-crypto": "~56.0.4",
40
- "expo-dev-client": "~56.0.18",
40
+ "expo-dev-client": "~56.0.20",
41
41
  "expo-device": "~56.0.4",
42
42
  "expo-document-picker": "~56.0.4",
43
43
  "expo-file-system": "~56.0.7",
@@ -46,55 +46,55 @@
46
46
  "expo-glass-effect": "~56.0.4",
47
47
  "expo-google-app-auth": "~8.3.0",
48
48
  "expo-haptics": "~56.0.3",
49
- "expo-image": "~56.0.9",
49
+ "expo-image": "~56.0.11",
50
50
  "expo-image-loader": "~56.0.3",
51
- "expo-image-manipulator": "~56.0.16",
52
- "expo-image-picker": "~56.0.15",
51
+ "expo-image-manipulator": "~56.0.18",
52
+ "expo-image-picker": "~56.0.17",
53
53
  "expo-intent-launcher": "~56.0.4",
54
- "expo-insights": "~56.0.15",
54
+ "expo-insights": "~56.0.17",
55
55
  "expo-keep-awake": "~56.0.3",
56
56
  "expo-linear-gradient": "~56.0.4",
57
- "expo-linking": "~56.0.13",
57
+ "expo-linking": "~56.0.14",
58
58
  "expo-local-authentication": "~56.0.4",
59
59
  "expo-localization": "~56.0.6",
60
- "expo-location": "~56.0.15",
60
+ "expo-location": "~56.0.17",
61
61
  "expo-mail-composer": "~56.0.4",
62
62
  "expo-manifests": "~56.0.4",
63
63
  "expo-maps": "~56.0.6",
64
64
  "expo-mcp": "~0.2.1",
65
65
  "expo-media-library": "~56.0.6",
66
66
  "expo-mesh-gradient": "~56.0.3",
67
- "expo-module-template": "~56.0.12",
68
- "expo-modules-core": "~56.0.14",
67
+ "expo-module-template": "~56.0.13",
68
+ "expo-modules-core": "~56.0.16",
69
69
  "expo-navigation-bar": "~56.0.3",
70
- "expo-network": "~56.0.4",
71
- "expo-notifications": "~56.0.15",
72
- "expo-observe": "~56.0.18",
70
+ "expo-network": "~56.0.5",
71
+ "expo-notifications": "~56.0.17",
72
+ "expo-observe": "~56.0.20",
73
73
  "expo-print": "~56.0.3",
74
74
  "expo-live-photo": "~56.0.3",
75
- "expo-router": "~56.2.8",
75
+ "expo-router": "~56.2.10",
76
76
  "expo-screen-capture": "~56.0.4",
77
77
  "expo-screen-orientation": "~56.0.5",
78
78
  "expo-secure-store": "~56.0.4",
79
- "expo-sensors": "~56.0.5",
80
- "expo-server": "~56.0.4",
81
- "expo-sharing": "~56.0.15",
79
+ "expo-sensors": "~56.0.6",
80
+ "expo-server": "~56.0.5",
81
+ "expo-sharing": "~56.0.17",
82
82
  "expo-sms": "~56.0.3",
83
83
  "expo-speech": "~56.0.3",
84
84
  "expo-splash-screen": "~56.0.10",
85
- "expo-sqlite": "~56.0.4",
85
+ "expo-sqlite": "~56.0.5",
86
86
  "expo-status-bar": "~56.0.4",
87
87
  "expo-store-review": "~56.0.3",
88
- "expo-symbols": "~56.0.5",
88
+ "expo-symbols": "~56.0.6",
89
89
  "expo-system-ui": "~56.0.5",
90
- "expo-task-manager": "~56.0.16",
90
+ "expo-task-manager": "~56.0.18",
91
91
  "expo-tracking-transparency": "~56.0.5",
92
- "expo-updates": "~56.0.17",
92
+ "expo-updates": "~56.0.19",
93
93
  "expo-video-thumbnails": "~56.0.3",
94
- "expo-video": "~56.1.2",
94
+ "expo-video": "~56.1.3",
95
95
  "expo-web-browser": "~56.0.5",
96
- "expo-widgets": "~56.0.16",
97
- "jest-expo": "~56.0.4",
96
+ "expo-widgets": "~56.0.18",
97
+ "jest-expo": "~56.0.5",
98
98
  "lottie-react-native": "~7.3.4",
99
99
  "react": "19.2.3",
100
100
  "react-dom": "19.2.3",
@@ -7,6 +7,11 @@
7
7
  @protocol RCTHostRuntimeDelegate;
8
8
 
9
9
  NS_SWIFT_NAME(ExpoReactNativeFactoryObjC)
10
+ #if !TARGET_OS_OSX
10
11
  @interface EXReactNativeFactory : RCTReactNativeFactory <RCTHostDelegate>
12
+ #else
13
+ // TODO: remove when bumping to react-native-macos 0.85
14
+ @interface EXReactNativeFactory : RCTReactNativeFactory <RCTHostDelegate, RCTHostRuntimeDelegate>
15
+ #endif
11
16
 
12
17
  @end
@@ -28,6 +28,14 @@
28
28
 
29
29
  #pragma mark - RCTHostDelegate
30
30
 
31
+ #if TARGET_OS_OSX
32
+ // TODO: remove when bumping react-native-macos to 0.85
33
+ - (void)hostDidStart:(nonnull RCTHost *)host
34
+ {
35
+ host.runtimeDelegate = self;
36
+ }
37
+ #endif
38
+
31
39
  // [JS thread]
32
40
  - (void)host:(nonnull RCTHost *)host didInitializeRuntime:(facebook::jsi::Runtime &)runtime
33
41
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo",
3
- "version": "56.0.8",
3
+ "version": "56.0.10",
4
4
  "description": "The Expo SDK",
5
5
  "main": "src/Expo.ts",
6
6
  "module": "src/Expo.ts",
@@ -68,25 +68,25 @@
68
68
  "homepage": "https://github.com/expo/expo/tree/main/packages/expo",
69
69
  "dependencies": {
70
70
  "@babel/runtime": "^7.20.0",
71
- "@expo/cli": "^56.1.13",
71
+ "@expo/cli": "^56.1.15",
72
72
  "@expo/config": "~56.0.9",
73
73
  "@expo/config-plugins": "~56.0.8",
74
74
  "@expo/devtools": "~56.0.2",
75
75
  "@expo/dom-webview": "~56.0.5",
76
- "@expo/fingerprint": "^0.19.3",
76
+ "@expo/fingerprint": "^0.19.4",
77
77
  "@expo/local-build-cache-provider": "^56.0.8",
78
- "@expo/log-box": "^56.0.12",
78
+ "@expo/log-box": "^56.0.13",
79
79
  "@expo/metro": "~56.0.0",
80
- "@expo/metro-config": "~56.0.13",
80
+ "@expo/metro-config": "~56.0.14",
81
81
  "@ungap/structured-clone": "^1.3.0",
82
- "babel-preset-expo": "~56.0.14",
83
- "expo-asset": "~56.0.15",
84
- "expo-constants": "~56.0.16",
82
+ "babel-preset-expo": "~56.0.15",
83
+ "expo-asset": "~56.0.17",
84
+ "expo-constants": "~56.0.18",
85
85
  "expo-file-system": "~56.0.7",
86
86
  "expo-font": "~56.0.5",
87
87
  "expo-keep-awake": "~56.0.3",
88
- "expo-modules-autolinking": "~56.0.14",
89
- "expo-modules-core": "~56.0.14",
88
+ "expo-modules-autolinking": "~56.0.15",
89
+ "expo-modules-core": "~56.0.16",
90
90
  "pretty-format": "^29.7.0",
91
91
  "react-refresh": "^0.14.2",
92
92
  "whatwg-url-minimum": "^0.1.2"
@@ -101,8 +101,8 @@
101
101
  "rimraf": "^6.1.2",
102
102
  "web-streams-polyfill": "^3.3.2",
103
103
  "@expo/dom-webview": "56.0.5",
104
- "expo-updates": "56.0.17",
105
- "@expo/metro-runtime": "56.0.13"
104
+ "@expo/metro-runtime": "56.0.15",
105
+ "expo-updates": "56.0.19"
106
106
  },
107
107
  "peerDependencies": {
108
108
  "@expo/dom-webview": "*",
@@ -139,7 +139,7 @@
139
139
  "scripts/resolveAppEntry.js"
140
140
  ]
141
141
  },
142
- "gitHead": "7df487c3b35d509e8988c87c0abcf8a7d76aa202",
142
+ "gitHead": "b1e94a5c1c5b19472a42ca25752a3533699bc46a",
143
143
  "scripts": {
144
144
  "build": "tsc",
145
145
  "clean": "rimraf build",
@@ -63,18 +63,35 @@ describe('asyncRequireModule', () => {
63
63
  }
64
64
 
65
65
  function asyncRequireImpl(moduleID, paths, moduleName) {
66
- var maybeLoadBundlePromise = maybeLoadBundle(moduleID, paths);
67
66
  var importAll = function() { return require.importAll(moduleID, moduleName); };
68
-
69
- if (maybeLoadBundlePromise != null) {
70
- return maybeLoadBundlePromise.then(importAll);
67
+ try {
68
+ // Try importing first to skip bundle loading when the bundle is already preloaded.
69
+ return importAll();
70
+ } catch (error) {
71
+ var maybeLoadBundlePromise = maybeLoadBundle(moduleID, paths);
72
+ if (maybeLoadBundlePromise != null) {
73
+ return maybeLoadBundlePromise.then(importAll);
74
+ }
75
+ throw error;
71
76
  }
72
-
73
- return importAll();
74
77
  }
75
78
 
76
- async function asyncRequire(moduleID, paths, moduleName) {
77
- return asyncRequireImpl(moduleID, paths, moduleName);
79
+ function asyncRequire(moduleID, paths, moduleName) {
80
+ var ret = asyncRequireImpl(moduleID, paths, moduleName);
81
+ if (!(ret instanceof Promise)) {
82
+ return {
83
+ _result: ret,
84
+ then: function(resolve, reject) {
85
+ return Promise.resolve(ret).then(resolve, reject);
86
+ },
87
+ };
88
+ }
89
+ return {
90
+ _result: ret,
91
+ then: function(resolve, reject) {
92
+ return ret.then(resolve, reject);
93
+ },
94
+ };
78
95
  }
79
96
 
80
97
  asyncRequire.unstable_importMaybeSync = function(moduleID, paths) {
@@ -112,7 +129,13 @@ describe('asyncRequireModule', () => {
112
129
  expect(result).toEqual({ default: 'module-42' });
113
130
  });
114
131
 
115
- it('passes moduleName through when bundle loading is required', async () => {
132
+ it('falls back to bundle load when importAll throws, then retries with moduleName', async () => {
133
+ mockImportAll
134
+ .mockImplementationOnce(() => {
135
+ throw new Error('Module not loaded');
136
+ })
137
+ .mockImplementationOnce(() => ({ default: 'module-42' }));
138
+
116
139
  let resolveBundle!: () => void;
117
140
  const bundlePromise = new Promise<void>((resolve) => {
118
141
  resolveBundle = resolve;
@@ -123,17 +146,62 @@ describe('asyncRequireModule', () => {
123
146
  const paths = { '42': '/bundles/my-module.bundle' };
124
147
  const resultPromise = asyncRequire(42, paths, 'my-module');
125
148
 
126
- // importAll should not have been called yet (waiting for bundle)
127
- expect(mockImportAll).not.toHaveBeenCalled();
149
+ // Initial importAll attempt happened; second is gated on the bundle promise.
150
+ expect(mockImportAll).toHaveBeenCalledTimes(1);
151
+ expect((globalThis as any).__loadBundleAsync).toHaveBeenCalledWith('/bundles/my-module.bundle');
128
152
 
129
- // Resolve the bundle loading
130
153
  resolveBundle();
131
154
  const result = await resultPromise;
132
155
 
156
+ expect(mockImportAll).toHaveBeenCalledTimes(2);
157
+ expect(mockImportAll).toHaveBeenLastCalledWith(42, 'my-module');
158
+ expect(result).toEqual({ default: 'module-42' });
159
+ });
160
+
161
+ it('does not load the bundle when importAll succeeds (preloaded bundle case)', async () => {
162
+ (globalThis as any).__loadBundleAsync = jest.fn(() => Promise.resolve());
163
+
164
+ const paths = { '42': '/bundles/my-module.bundle' };
165
+ const result = await asyncRequire(42, paths, 'my-module');
166
+
133
167
  expect(mockImportAll).toHaveBeenCalledWith(42, 'my-module');
168
+ expect((globalThis as any).__loadBundleAsync).not.toHaveBeenCalled();
134
169
  expect(result).toEqual({ default: 'module-42' });
135
170
  });
136
171
 
172
+ it('re-throws the import error when no bundle path is configured', () => {
173
+ mockImportAll.mockImplementationOnce(() => {
174
+ throw new Error('Module not loaded');
175
+ });
176
+
177
+ expect(() => asyncRequire(42, null, 'my-module')).toThrow('Module not loaded');
178
+ });
179
+
180
+ describe('thenable return value', () => {
181
+ it('exposes a synchronous _result when no bundle load was needed', () => {
182
+ const ret = asyncRequire(42, null, 'my-module');
183
+
184
+ expect(typeof ret.then).toBe('function');
185
+ expect(ret._result).toEqual({ default: 'module-42' });
186
+ });
187
+
188
+ it('exposes a Promise _result when a bundle load was needed', async () => {
189
+ mockImportAll
190
+ .mockImplementationOnce(() => {
191
+ throw new Error('Module not loaded');
192
+ })
193
+ .mockImplementationOnce(() => ({ default: 'module-42' }));
194
+
195
+ (globalThis as any).__loadBundleAsync = jest.fn(() => Promise.resolve());
196
+
197
+ const ret = asyncRequire(42, { '42': '/bundles/my-module.bundle' }, 'my-module');
198
+
199
+ expect(typeof ret.then).toBe('function');
200
+ expect(ret._result).toBeInstanceOf(Promise);
201
+ await expect(ret._result).resolves.toEqual({ default: 'module-42' });
202
+ });
203
+ });
204
+
137
205
  describe('unstable_importMaybeSync', () => {
138
206
  it('returns synchronously when no bundle loading needed', () => {
139
207
  const result = asyncRequire.unstable_importMaybeSync(42, null);
@@ -67,22 +67,43 @@ function asyncRequireImpl<T>(
67
67
  paths: DependencyMapPaths,
68
68
  moduleName?: string
69
69
  ): Promise<T> | T {
70
- const maybeLoadBundlePromise = maybeLoadBundle(moduleID, paths);
71
70
  const importAll = () => (require as unknown as MetroRequire).importAll<T>(moduleID, moduleName);
72
-
73
- if (maybeLoadBundlePromise != null) {
74
- return maybeLoadBundlePromise.then(importAll);
71
+ try {
72
+ // Try importing first to prevent double-loading script when the page already preloaded it
73
+ return importAll();
74
+ } catch (error) {
75
+ const maybeLoadBundlePromise = maybeLoadBundle(moduleID, paths);
76
+ if (maybeLoadBundlePromise != null) {
77
+ return maybeLoadBundlePromise.then(importAll);
78
+ }
79
+ throw error;
75
80
  }
76
-
77
- return importAll();
78
81
  }
79
82
 
80
- async function asyncRequire<T>(
83
+ function asyncRequire<T>(
81
84
  moduleID: number,
82
85
  paths: DependencyMapPaths,
83
86
  moduleName?: string
84
- ): Promise<T> {
85
- return asyncRequireImpl<T>(moduleID, paths, moduleName);
87
+ ): PromiseLike<T> & { _result?: T | Promise<T> } {
88
+ const ret = asyncRequireImpl<T>(moduleID, paths, moduleName);
89
+ if (!(ret instanceof Promise)) {
90
+ // We return a thenable with an added `unstable_importMaybeSync`-like
91
+ // `_result` property to bypass this being force-converted to a promise
92
+ // for rehydration
93
+ return {
94
+ _result: ret,
95
+ then(resolve, reject) {
96
+ return Promise.resolve(ret).then(resolve, reject);
97
+ },
98
+ };
99
+ } else {
100
+ return {
101
+ _result: ret,
102
+ then(resolve, reject) {
103
+ return ret.then(resolve, reject);
104
+ },
105
+ };
106
+ }
86
107
  }
87
108
 
88
109
  // Synchronous version of asyncRequire, which can still return a promise
@@ -1,3 +1,5 @@
1
+ import type * as React from 'react';
2
+
1
3
  import HMRClient from './hmr';
2
4
 
3
5
  if (typeof window !== 'undefined') {
@@ -17,11 +19,64 @@ if (typeof window !== 'undefined') {
17
19
  LEVELS.forEach((level) => {
18
20
  const originalFunction = console[level];
19
21
  console[level] = function (...args: any[]) {
20
- HMRClient.log(level, args);
22
+ HMRClient.log(level, level === 'error' ? addErrorStacks(args, true) : args);
21
23
  originalFunction.apply(console, args);
22
24
  };
23
25
  });
26
+
27
+ window.addEventListener('error', (event) => {
28
+ // Not capturing current stack as it would only point to this function,
29
+ // the stack chain is preserved by the browser.
30
+ HMRClient.log('error', addErrorStacks([event.error]));
31
+ });
32
+
33
+ window.addEventListener('unhandledrejection', (event) => {
34
+ // Not capturing current stack as it would only point to this function,
35
+ // the stack chain is preserved by the browser.
36
+ HMRClient.log('error', addErrorStacks([event.reason]));
37
+ });
24
38
  }
25
39
 
26
40
  // This is called native on native platforms
27
41
  HMRClient.setup({ isEnabled: true });
42
+
43
+ function addErrorStacks(data: unknown[], shouldCaptureCurrentStack = false) {
44
+ const dataWithStacks = [...data];
45
+ let hasStack = false;
46
+ data.forEach((item) => {
47
+ // on native handled in packages/@expo/metro-runtime/src/metroServerLogs.native.ts
48
+ // https://github.com/expo/expo/blob/118528654c982b6df2f4b3e73bbf2ae0b78d84a2/packages/%40expo/metro-runtime/src/metroServerLogs.native.ts#L30
49
+ // this differs from native implementation where error from native modules
50
+ // would not pass instanceof Error check
51
+ if (item instanceof Error && item.stack) {
52
+ hasStack = true;
53
+ dataWithStacks.push(item.stack);
54
+ }
55
+ });
56
+
57
+ if (!hasStack && shouldCaptureCurrentStack) {
58
+ // for console.* to point to the call site
59
+ const stack = captureCurrentStack();
60
+ if (typeof stack === 'string') {
61
+ hasStack = true;
62
+ dataWithStacks.push(stack);
63
+ }
64
+ }
65
+
66
+ const react = require('react') as typeof React;
67
+ const componentStack = react.captureOwnerStack?.();
68
+ if (componentStack) {
69
+ dataWithStacks.push(componentStack);
70
+ }
71
+
72
+ return dataWithStacks;
73
+ }
74
+
75
+ class NamelessError extends Error {
76
+ name = '';
77
+ }
78
+
79
+ function captureCurrentStack() {
80
+ // If you're reading this, look deeper into the call stack to find the actual error source.
81
+ return new NamelessError().stack;
82
+ }
package/template.tgz CHANGED
Binary file