expo 54.0.0-canary-20250722-599a28f → 54.0.0-canary-20250826-f475166
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/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/ExpoReactHostFactory.kt +1 -7
- package/android/src/main/java/expo/modules/ReactActivityDelegateWrapper.kt +33 -8
- package/android/src/main/java/expo/modules/ReactNativeHostWrapper.kt +0 -5
- package/android/src/test/resources/robolectric.properties +1 -0
- package/build/Expo.d.ts +9 -1
- package/build/Expo.d.ts.map +1 -1
- package/build/async-require/fetchAsync.native.d.ts.map +1 -1
- package/build/async-require/hmr.d.ts.map +1 -1
- package/build/winter/FormData.d.ts.map +1 -1
- package/build/winter/fetch/FetchResponse.d.ts +2 -2
- package/build/winter/fetch/FetchResponse.d.ts.map +1 -1
- package/build/winter/fetch/NativeRequest.d.ts +1 -1
- package/build/winter/fetch/NativeRequest.d.ts.map +1 -1
- package/build/winter/fetch/RequestUtils.d.ts.map +1 -1
- package/build/winter/fetch/convertFormData.d.ts.map +1 -1
- package/build/winter/installGlobal.d.ts +25 -0
- package/build/winter/installGlobal.d.ts.map +1 -0
- package/bundledNativeModules.json +97 -96
- package/devtools.d.ts +1 -1
- package/devtools.js +1 -1
- package/internal/babel-preset.d.ts +2 -0
- package/internal/babel-preset.js +2 -0
- package/internal/install-global.d.ts +2 -0
- package/internal/install-global.js +5 -0
- package/internal/unstable-autolinking-exports.d.ts +2 -0
- package/internal/unstable-autolinking-exports.js +2 -0
- package/internal/unstable-expo-updates-cli-exports.d.ts +2 -0
- package/internal/unstable-expo-updates-cli-exports.js +2 -0
- package/ios/AppDelegates/ExpoAppDelegate.swift +1 -1
- package/ios/AppDelegates/ExpoReactNativeFactory.swift +15 -2
- package/ios/AppDelegates/ExpoReactNativeFactoryDelegate.swift +1 -1
- package/ios/AppDelegates/RCTAppDelegateUmbrella.h +5 -0
- package/ios/Fetch/ExpoFetchCustomExtension.swift +1 -1
- package/ios/Fetch/ExpoFetchModule.swift +3 -3
- package/ios/Fetch/ExpoURLSessionTask.swift +2 -2
- package/ios/Fetch/NativeRequest.swift +1 -1
- package/ios/Fetch/NativeResponse.swift +2 -2
- package/package.json +29 -27
- package/src/Expo.ts +4 -0
- package/src/async-require/fetchAsync.native.ts +48 -10
- package/src/async-require/hmr.ts +1 -2
- package/src/async-require/messageSocket.native.ts +0 -3
- package/src/winter/FormData.ts +1 -9
- package/src/winter/__tests__/structuredClone.test.ios.ts +27 -0
- package/src/winter/fetch/FetchResponse.ts +4 -4
- package/src/winter/fetch/NativeRequest.ts +1 -1
- package/src/winter/fetch/RequestUtils.ts +15 -1
- package/src/winter/fetch/__tests__/RequestUtils-test.ts +7 -7
- package/src/winter/fetch/__tests__/convertFormData-test.native.ts +1 -3
- package/src/winter/fetch/convertFormData.ts +56 -27
- package/src/winter/installGlobal.ts +109 -0
- package/src/winter/runtime.native.ts +4 -20
- package/template.tgz +0 -0
- package/build/devtools/DevToolsPluginClient.d.ts +0 -72
- package/build/devtools/DevToolsPluginClient.d.ts.map +0 -1
- package/build/devtools/DevToolsPluginClientFactory.d.ts +0 -16
- package/build/devtools/DevToolsPluginClientFactory.d.ts.map +0 -1
- package/build/devtools/DevToolsPluginClientImplApp.d.ts +0 -15
- package/build/devtools/DevToolsPluginClientImplApp.d.ts.map +0 -1
- package/build/devtools/DevToolsPluginClientImplBrowser.d.ts +0 -14
- package/build/devtools/DevToolsPluginClientImplBrowser.d.ts.map +0 -1
- package/build/devtools/MessageFramePacker.d.ts +0 -50
- package/build/devtools/MessageFramePacker.d.ts.map +0 -1
- package/build/devtools/ProtocolVersion.d.ts +0 -7
- package/build/devtools/ProtocolVersion.d.ts.map +0 -1
- package/build/devtools/WebSocketBackingStore.d.ts +0 -10
- package/build/devtools/WebSocketBackingStore.d.ts.map +0 -1
- package/build/devtools/WebSocketWithReconnect.d.ts +0 -81
- package/build/devtools/WebSocketWithReconnect.d.ts.map +0 -1
- package/build/devtools/devtools.types.d.ts +0 -42
- package/build/devtools/devtools.types.d.ts.map +0 -1
- package/build/devtools/getConnectionInfo.d.ts +0 -6
- package/build/devtools/getConnectionInfo.d.ts.map +0 -1
- package/build/devtools/getConnectionInfo.native.d.ts +0 -6
- package/build/devtools/getConnectionInfo.native.d.ts.map +0 -1
- package/build/devtools/index.d.ts +0 -12
- package/build/devtools/index.d.ts.map +0 -1
- package/build/devtools/logger.d.ts +0 -6
- package/build/devtools/logger.d.ts.map +0 -1
- package/src/devtools/DevToolsPluginClient.ts +0 -240
- package/src/devtools/DevToolsPluginClientFactory.ts +0 -73
- package/src/devtools/DevToolsPluginClientImplApp.ts +0 -56
- package/src/devtools/DevToolsPluginClientImplBrowser.ts +0 -38
- package/src/devtools/MessageFramePacker.ts +0 -235
- package/src/devtools/ProtocolVersion.ts +0 -6
- package/src/devtools/WebSocketBackingStore.ts +0 -10
- package/src/devtools/WebSocketWithReconnect.ts +0 -318
- package/src/devtools/__tests__/DevToolsPluginClient-test.ts +0 -285
- package/src/devtools/__tests__/DevToolsPluginClientFactory-test.ts +0 -120
- package/src/devtools/__tests__/MessageFramePack-test.node.ts +0 -157
- package/src/devtools/__tests__/MockWebSocket.ts +0 -100
- package/src/devtools/__tests__/WebSocketWithReconnect-test.node.ts +0 -184
- package/src/devtools/__tests__/logger-test.ts +0 -20
- package/src/devtools/devtools.types.ts +0 -50
- package/src/devtools/getConnectionInfo.native.ts +0 -18
- package/src/devtools/getConnectionInfo.ts +0 -16
- package/src/devtools/index.ts +0 -53
- package/src/devtools/logger.ts +0 -29
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import React
|
|
4
4
|
|
|
5
|
-
public class ExpoReactNativeFactory: RCTReactNativeFactory {
|
|
5
|
+
@MainActor public class ExpoReactNativeFactory: RCTReactNativeFactory {
|
|
6
6
|
private let reactDelegate = ExpoReactDelegate(handlers: ExpoAppDelegateSubscriberRepository.reactDelegateHandlers)
|
|
7
7
|
|
|
8
|
+
@objc public override init(delegate: any RCTReactNativeFactoryDelegate) {
|
|
9
|
+
let releaseLevel = (Bundle.main.object(forInfoDictionaryKey: "ReactNativeReleaseLevel") as? String)
|
|
10
|
+
.flatMap { [
|
|
11
|
+
"canary": RCTReleaseLevel.Canary,
|
|
12
|
+
"experimental": RCTReleaseLevel.Experimental,
|
|
13
|
+
"stable": RCTReleaseLevel.Stable
|
|
14
|
+
][$0.lowercased()]
|
|
15
|
+
}
|
|
16
|
+
?? RCTReleaseLevel.Stable
|
|
17
|
+
|
|
18
|
+
super.init(delegate: delegate, releaseLevel: releaseLevel)
|
|
19
|
+
}
|
|
20
|
+
|
|
8
21
|
@objc func createRCTRootViewFactory() -> RCTRootViewFactory {
|
|
9
22
|
// Alan: This is temporary. We need to cast to ExpoReactNativeFactoryDelegate here because currently, if you extend RCTReactNativeFactory
|
|
10
23
|
// from Swift, customizeRootView will not work on the new arch because the cast to RCTRootView will never
|
|
@@ -14,7 +14,12 @@
|
|
|
14
14
|
#endif // USE_HERMES
|
|
15
15
|
#endif // __has_include(<reacthermes/HermesExecutorFactory.h>)
|
|
16
16
|
|
|
17
|
+
#if __has_include(<React_RCTAppDelegate/React-RCTAppDelegate-umbrella.h>)
|
|
17
18
|
#import <React_RCTAppDelegate/React-RCTAppDelegate-umbrella.h>
|
|
19
|
+
#else
|
|
20
|
+
#import <React_RCTAppDelegate/React_RCTAppDelegate-umbrella.h>
|
|
21
|
+
#endif
|
|
22
|
+
|
|
18
23
|
|
|
19
24
|
#if INLINE_USE_HERMES
|
|
20
25
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
@objc(EXFetchCustomExtension)
|
|
7
7
|
public class ExpoFetchCustomExtension: NSObject {
|
|
8
|
-
@objc
|
|
8
|
+
@MainActor @objc
|
|
9
9
|
public static func setCustomURLSessionConfigurationProvider(_ provider: NSURLSessionConfigurationProvider?) {
|
|
10
10
|
urlSessionConfigurationProvider = provider
|
|
11
11
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Copyright 2015-present 650 Industries. All rights reserved.
|
|
2
2
|
|
|
3
|
-
import ExpoModulesCore
|
|
3
|
+
@preconcurrency import ExpoModulesCore
|
|
4
4
|
|
|
5
5
|
private let fetchRequestQueue = DispatchQueue(label: "expo.modules.fetch.RequestQueue")
|
|
6
|
-
internal var urlSessionConfigurationProvider: NSURLSessionConfigurationProvider?
|
|
6
|
+
@MainActor internal var urlSessionConfigurationProvider: NSURLSessionConfigurationProvider?
|
|
7
7
|
|
|
8
|
-
public final class ExpoFetchModule: Module {
|
|
8
|
+
@MainActor public final class ExpoFetchModule: @preconcurrency Module {
|
|
9
9
|
private lazy var urlSession = createURLSession()
|
|
10
10
|
private let urlSessionDelegate: URLSessionSessionDelegateProxy
|
|
11
11
|
|
|
@@ -5,7 +5,7 @@ import ExpoModulesCore
|
|
|
5
5
|
/**
|
|
6
6
|
An URLSessionDataTask wrapper.
|
|
7
7
|
*/
|
|
8
|
-
internal final class ExpoURLSessionTask: NSObject, URLSessionTaskDelegate, URLSessionDataDelegate {
|
|
8
|
+
internal final class ExpoURLSessionTask: NSObject, URLSessionTaskDelegate, URLSessionDataDelegate, @unchecked Sendable {
|
|
9
9
|
private let delegate: ExpoURLSessionTaskDelegate
|
|
10
10
|
private var task: URLSessionDataTask?
|
|
11
11
|
|
|
@@ -88,7 +88,7 @@ internal final class ExpoURLSessionTask: NSObject, URLSessionTaskDelegate, URLSe
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
internal protocol ExpoURLSessionTaskDelegate: AnyObject {
|
|
91
|
+
internal protocol ExpoURLSessionTaskDelegate: AnyObject, Sendable {
|
|
92
92
|
func urlSessionDidStart(_ session: ExpoURLSessionTask)
|
|
93
93
|
func urlSession(_ session: ExpoURLSessionTask, didReceive response: URLResponse)
|
|
94
94
|
func urlSession(_ session: ExpoURLSessionTask, didReceive data: Data)
|
|
@@ -5,7 +5,7 @@ import ExpoModulesCore
|
|
|
5
5
|
/**
|
|
6
6
|
A SharedObject for request.
|
|
7
7
|
*/
|
|
8
|
-
internal final class NativeRequest: SharedObject {
|
|
8
|
+
internal final class NativeRequest: SharedObject, @unchecked Sendable {
|
|
9
9
|
internal let response: NativeResponse
|
|
10
10
|
internal let task: ExpoURLSessionTask
|
|
11
11
|
|
|
@@ -5,7 +5,7 @@ import ExpoModulesCore
|
|
|
5
5
|
/**
|
|
6
6
|
A SharedObject for response.
|
|
7
7
|
*/
|
|
8
|
-
internal final class NativeResponse: SharedObject, ExpoURLSessionTaskDelegate {
|
|
8
|
+
internal final class NativeResponse: SharedObject, ExpoURLSessionTaskDelegate, @unchecked Sendable {
|
|
9
9
|
internal let sink: ResponseSink
|
|
10
10
|
|
|
11
11
|
private let dispatchQueue: DispatchQueue
|
|
@@ -72,7 +72,7 @@ internal final class NativeResponse: SharedObject, ExpoURLSessionTaskDelegate {
|
|
|
72
72
|
/**
|
|
73
73
|
Waits for given states and when it meets the requirement, executes the callback.
|
|
74
74
|
*/
|
|
75
|
-
func waitFor(states: [ResponseState], callback: @escaping (ResponseState) -> Void) {
|
|
75
|
+
func waitFor(states: [ResponseState], callback: @escaping @Sendable (ResponseState) -> Void) {
|
|
76
76
|
if states.contains(state) {
|
|
77
77
|
callback(state)
|
|
78
78
|
return
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo",
|
|
3
|
-
"version": "54.0.0-canary-
|
|
3
|
+
"version": "54.0.0-canary-20250826-f475166",
|
|
4
4
|
"description": "The Expo SDK",
|
|
5
5
|
"main": "src/Expo.ts",
|
|
6
6
|
"module": "src/Expo.ts",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"android",
|
|
21
21
|
"bin",
|
|
22
22
|
"build",
|
|
23
|
+
"internal",
|
|
23
24
|
"ios",
|
|
24
25
|
"scripts",
|
|
25
26
|
"src",
|
|
@@ -43,7 +44,8 @@
|
|
|
43
44
|
"requiresExtraSetup.json",
|
|
44
45
|
"tsconfig.base.json",
|
|
45
46
|
"types",
|
|
46
|
-
"virtual"
|
|
47
|
+
"virtual",
|
|
48
|
+
"template.tgz"
|
|
47
49
|
],
|
|
48
50
|
"scripts": {
|
|
49
51
|
"build": "expo-module build",
|
|
@@ -73,41 +75,41 @@
|
|
|
73
75
|
"homepage": "https://github.com/expo/expo/tree/main/packages/expo",
|
|
74
76
|
"dependencies": {
|
|
75
77
|
"@babel/runtime": "^7.20.0",
|
|
76
|
-
"@expo/cli": "
|
|
77
|
-
"@expo/config": "
|
|
78
|
-
"@expo/config-plugins": "
|
|
79
|
-
"@expo/
|
|
80
|
-
"@expo/
|
|
81
|
-
"@expo/
|
|
82
|
-
"
|
|
83
|
-
"expo-
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"expo-
|
|
87
|
-
"expo-
|
|
88
|
-
"expo-
|
|
89
|
-
"expo-
|
|
78
|
+
"@expo/cli": "0.26.4-canary-20250826-f475166",
|
|
79
|
+
"@expo/config": "12.0.5-canary-20250826-f475166",
|
|
80
|
+
"@expo/config-plugins": "11.0.5-canary-20250826-f475166",
|
|
81
|
+
"@expo/devtools": "0.1.4-canary-20250826-f475166",
|
|
82
|
+
"@expo/fingerprint": "0.14.4-canary-20250826-f475166",
|
|
83
|
+
"@expo/metro": "~0.1.1",
|
|
84
|
+
"@expo/metro-config": "0.21.6-canary-20250826-f475166",
|
|
85
|
+
"@expo/vector-icons": "^15.0.0",
|
|
86
|
+
"@ungap/structured-clone": "^1.3.0",
|
|
87
|
+
"babel-preset-expo": "14.0.4-canary-20250826-f475166",
|
|
88
|
+
"expo-asset": "12.0.5-canary-20250826-f475166",
|
|
89
|
+
"expo-constants": "18.0.5-canary-20250826-f475166",
|
|
90
|
+
"expo-file-system": "19.0.6-canary-20250826-f475166",
|
|
91
|
+
"expo-font": "14.0.4-canary-20250826-f475166",
|
|
92
|
+
"expo-keep-awake": "15.0.4-canary-20250826-f475166",
|
|
93
|
+
"expo-modules-autolinking": "3.0.4-canary-20250826-f475166",
|
|
94
|
+
"expo-modules-core": "3.0.7-canary-20250826-f475166",
|
|
90
95
|
"pretty-format": "^29.7.0",
|
|
91
|
-
"react-
|
|
96
|
+
"react-refresh": "^0.14.2",
|
|
92
97
|
"whatwg-url-without-unicode": "8.0.0-3"
|
|
93
98
|
},
|
|
94
99
|
"devDependencies": {
|
|
95
100
|
"@types/node": "^22.14.0",
|
|
96
|
-
"@types/react": "~19.
|
|
97
|
-
"@types/react-test-renderer": "
|
|
98
|
-
"expo-module-scripts": "
|
|
101
|
+
"@types/react": "~19.1.10",
|
|
102
|
+
"@types/react-test-renderer": "~19.1.0",
|
|
103
|
+
"expo-module-scripts": "5.0.4-canary-20250826-f475166",
|
|
99
104
|
"react": "19.1.0",
|
|
100
105
|
"react-dom": "19.1.0",
|
|
101
|
-
"react-native": "0.
|
|
102
|
-
"web-streams-polyfill": "^3.3.2"
|
|
103
|
-
"ws": "^8.18.0"
|
|
106
|
+
"react-native": "0.81.0",
|
|
107
|
+
"web-streams-polyfill": "^3.3.2"
|
|
104
108
|
},
|
|
105
109
|
"peerDependencies": {
|
|
106
|
-
"@expo/dom-webview": "0.
|
|
107
|
-
"@expo/metro-runtime": "6.
|
|
108
|
-
"metro-runtime": "*",
|
|
110
|
+
"@expo/dom-webview": "0.2.4-canary-20250826-f475166",
|
|
111
|
+
"@expo/metro-runtime": "6.1.1-canary-20250826-f475166",
|
|
109
112
|
"react": "*",
|
|
110
|
-
"react-refresh": "*",
|
|
111
113
|
"react-native": "*",
|
|
112
114
|
"react-native-webview": "*"
|
|
113
115
|
},
|
package/src/Expo.ts
CHANGED
|
@@ -21,9 +21,13 @@ export {
|
|
|
21
21
|
} from 'expo-modules-core';
|
|
22
22
|
|
|
23
23
|
export type {
|
|
24
|
+
/** @deprecated Move to `SharedRef` with a type-only import instead */
|
|
24
25
|
SharedRef as SharedRefType,
|
|
26
|
+
/** @deprecated Move to `EventEmitter` with a type-only import instead */
|
|
25
27
|
EventEmitter as EventEmitterType,
|
|
28
|
+
/** @deprecated Move to `NativeModule` with a type-only import instead */
|
|
26
29
|
NativeModule as NativeModuleType,
|
|
30
|
+
/** @deprecated Move to `SharedObject` with a type-only import instead */
|
|
27
31
|
SharedObject as SharedObjectType,
|
|
28
32
|
} from 'expo-modules-core/types';
|
|
29
33
|
|
|
@@ -7,10 +7,27 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
// @ts-expect-error
|
|
10
|
-
import Networking from 'react-native
|
|
10
|
+
import { Networking } from 'react-native';
|
|
11
11
|
|
|
12
12
|
type Subscriber = { remove: () => void };
|
|
13
13
|
|
|
14
|
+
class LoadBundleFromServerError extends Error {
|
|
15
|
+
name = 'LoadBundleFromServerError';
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
message: string,
|
|
19
|
+
public url: string,
|
|
20
|
+
public isTimeout: boolean,
|
|
21
|
+
options?: ErrorOptions
|
|
22
|
+
) {
|
|
23
|
+
super(message, options);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class LoadBundleFromServerRequestError extends LoadBundleFromServerError {
|
|
28
|
+
name = 'LoadBundleFromServerRequestError';
|
|
29
|
+
}
|
|
30
|
+
|
|
14
31
|
export function fetchAsync(
|
|
15
32
|
url: string
|
|
16
33
|
): Promise<{ body: string; status: number; headers: Record<string, string> }> {
|
|
@@ -21,9 +38,10 @@ export function fetchAsync(
|
|
|
21
38
|
let dataListener: Subscriber | null = null;
|
|
22
39
|
let completeListener: Subscriber | null = null;
|
|
23
40
|
let responseListener: Subscriber | null = null;
|
|
41
|
+
let incrementalDataListener: Subscriber | null = null;
|
|
24
42
|
return new Promise<{ body: string; status: number; headers: Record<string, string> }>(
|
|
25
43
|
(resolve, reject) => {
|
|
26
|
-
const addListener = Networking.addListener.bind(
|
|
44
|
+
const addListener = Networking.addListener.bind() as (
|
|
27
45
|
event: string,
|
|
28
46
|
callback: (props: [string, any, any]) => any
|
|
29
47
|
) => Subscriber;
|
|
@@ -32,6 +50,18 @@ export function fetchAsync(
|
|
|
32
50
|
responseText = response;
|
|
33
51
|
}
|
|
34
52
|
});
|
|
53
|
+
incrementalDataListener = addListener(
|
|
54
|
+
'didReceiveNetworkIncrementalData',
|
|
55
|
+
([requestId, data]) => {
|
|
56
|
+
if (requestId === id) {
|
|
57
|
+
if (responseText != null) {
|
|
58
|
+
responseText += data;
|
|
59
|
+
} else {
|
|
60
|
+
responseText = data;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
);
|
|
35
65
|
responseListener = addListener(
|
|
36
66
|
'didReceiveNetworkResponse',
|
|
37
67
|
([requestId, status, responseHeaders]) => {
|
|
@@ -41,15 +71,22 @@ export function fetchAsync(
|
|
|
41
71
|
}
|
|
42
72
|
}
|
|
43
73
|
);
|
|
44
|
-
completeListener = addListener(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
74
|
+
completeListener = addListener(
|
|
75
|
+
'didCompleteNetworkResponse',
|
|
76
|
+
([requestId, error, isTimeout]) => {
|
|
77
|
+
if (requestId === id) {
|
|
78
|
+
if (error) {
|
|
79
|
+
reject(
|
|
80
|
+
new LoadBundleFromServerRequestError('Could not load bundle', url, isTimeout, {
|
|
81
|
+
cause: error,
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
} else {
|
|
85
|
+
resolve({ body: responseText!, status: statusCode!, headers });
|
|
86
|
+
}
|
|
50
87
|
}
|
|
51
88
|
}
|
|
52
|
-
|
|
89
|
+
);
|
|
53
90
|
(Networking.sendRequest as any)(
|
|
54
91
|
'GET',
|
|
55
92
|
'asyncRequest',
|
|
@@ -59,7 +96,7 @@ export function fetchAsync(
|
|
|
59
96
|
},
|
|
60
97
|
'',
|
|
61
98
|
'text',
|
|
62
|
-
|
|
99
|
+
true,
|
|
63
100
|
0,
|
|
64
101
|
(requestId: string) => {
|
|
65
102
|
id = requestId;
|
|
@@ -71,5 +108,6 @@ export function fetchAsync(
|
|
|
71
108
|
dataListener?.remove();
|
|
72
109
|
completeListener?.remove();
|
|
73
110
|
responseListener?.remove();
|
|
111
|
+
incrementalDataListener?.remove();
|
|
74
112
|
});
|
|
75
113
|
}
|
package/src/async-require/hmr.ts
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
* Based on this but with web support:
|
|
9
9
|
* https://github.com/facebook/react-native/blob/086714b02b0fb838dee5a66c5bcefe73b53cf3df/Libraries/Utilities/HMRClient.js
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
import MetroHMRClient from 'metro-runtime/src/modules/HMRClient';
|
|
11
|
+
import MetroHMRClient from '@expo/metro/metro-runtime/modules/HMRClient';
|
|
13
12
|
import prettyFormat, { plugins } from 'pretty-format';
|
|
14
13
|
import { DeviceEventEmitter } from 'react-native';
|
|
15
14
|
|
package/src/winter/FormData.ts
CHANGED
|
@@ -74,15 +74,7 @@ function normalizeArgs(
|
|
|
74
74
|
): [string, File | string] {
|
|
75
75
|
if (value instanceof Blob) {
|
|
76
76
|
// @ts-expect-error: `Blob.data.blobId` is react-native's proprietary property.
|
|
77
|
-
|
|
78
|
-
// For react-native created Blob objects,
|
|
79
|
-
// we need to keep its original form as-is without breaking functionality.
|
|
80
|
-
// However, we need to pass `name` for our file name handling.
|
|
81
|
-
// @ts-expect-error: Mutating the Blob object to add the `name` property.
|
|
82
|
-
value.name = blobFilename ?? 'blob';
|
|
83
|
-
} else {
|
|
84
|
-
value = { type: value.type, name: blobFilename ?? 'blob', blob: value };
|
|
85
|
-
}
|
|
77
|
+
value.name = blobFilename ?? blob.name ?? 'blob';
|
|
86
78
|
} else if (typeof value !== 'object') {
|
|
87
79
|
value = String(value);
|
|
88
80
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
describe('structuredClone', () => {
|
|
2
|
+
it(`uses the Expo built-in APIs`, () => {
|
|
3
|
+
expect((structuredClone as any)[Symbol.for('expo.builtin')]).toBe(true);
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
it(`clones primitives`, () => {
|
|
7
|
+
const obj = {
|
|
8
|
+
a: 1,
|
|
9
|
+
b: 'string',
|
|
10
|
+
c: [1, 2, 3],
|
|
11
|
+
d: { nested: true },
|
|
12
|
+
e: new Date(),
|
|
13
|
+
f: /regex/,
|
|
14
|
+
g: new Map([['key', 'value']]),
|
|
15
|
+
h: new Set([1, 2, 3]),
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const cloned = structuredClone(obj);
|
|
19
|
+
|
|
20
|
+
expect(cloned).toEqual(obj);
|
|
21
|
+
expect(cloned).not.toBe(obj); // Ensure it's a deep clone
|
|
22
|
+
expect(cloned.e).toBeInstanceOf(Date);
|
|
23
|
+
expect(cloned.f).toBeInstanceOf(RegExp);
|
|
24
|
+
expect(cloned.g).toBeInstanceOf(Map);
|
|
25
|
+
expect(cloned.h).toBeInstanceOf(Set);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -14,14 +14,14 @@ type UniversalFormData = globalThis.FormData & RNFormData;
|
|
|
14
14
|
*/
|
|
15
15
|
export class FetchResponse extends ConcreteNativeResponse implements Response {
|
|
16
16
|
private streamingState: 'none' | 'started' | 'completed' = 'none';
|
|
17
|
-
private bodyStream: ReadableStream<Uint8Array
|
|
17
|
+
private bodyStream: ReadableStream<Uint8Array<ArrayBuffer>> | null = null;
|
|
18
18
|
|
|
19
19
|
constructor(private readonly abortCleanupFunction: AbortSubscriptionCleanupFunction) {
|
|
20
20
|
super();
|
|
21
21
|
this.addListener('readyForJSFinalization', this.finalize);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
get body(): ReadableStream<Uint8Array
|
|
24
|
+
get body(): ReadableStream<Uint8Array<ArrayBuffer>> | null {
|
|
25
25
|
if (this.bodyStream == null) {
|
|
26
26
|
const response = this;
|
|
27
27
|
|
|
@@ -35,7 +35,7 @@ export class FetchResponse extends ConcreteNativeResponse implements Response {
|
|
|
35
35
|
if (response.streamingState === 'completed') {
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
|
-
response.addListener('didReceiveResponseData', (data: Uint8Array) => {
|
|
38
|
+
response.addListener('didReceiveResponseData', (data: Uint8Array<ArrayBuffer>) => {
|
|
39
39
|
if (!isControllerClosed) {
|
|
40
40
|
controller.enqueue(data);
|
|
41
41
|
}
|
|
@@ -113,7 +113,7 @@ export class FetchResponse extends ConcreteNativeResponse implements Response {
|
|
|
113
113
|
return JSON.parse(text);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
async bytes(): Promise<Uint8Array
|
|
116
|
+
async bytes(): Promise<Uint8Array<ArrayBuffer>> {
|
|
117
117
|
return new Uint8Array(await this.arrayBuffer());
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -32,7 +32,7 @@ export declare class NativeResponse extends SharedObject<NativeResponseEvents> {
|
|
|
32
32
|
readonly statusText: string;
|
|
33
33
|
readonly url: string;
|
|
34
34
|
readonly redirected: boolean;
|
|
35
|
-
startStreaming(): Promise<Uint8Array | null>;
|
|
35
|
+
startStreaming(): Promise<Uint8Array<ArrayBuffer> | null>;
|
|
36
36
|
cancelStreaming(reason: string): void;
|
|
37
37
|
arrayBuffer(): Promise<ArrayBuffer>;
|
|
38
38
|
text(): Promise<string>;
|
|
@@ -31,6 +31,20 @@ export async function convertReadableStreamToUint8ArrayAsync(
|
|
|
31
31
|
return result;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Also accept instances that don't extend blob but have required methods (for filesystem and custom blob module)
|
|
35
|
+
function isBlob(obj: any): obj is Blob {
|
|
36
|
+
if (typeof obj !== 'object') {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (!('arrayBuffer' in obj)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (!('type' in obj)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
/**
|
|
35
49
|
* Normalize a BodyInit object to a Uint8Array for NativeRequest
|
|
36
50
|
*/
|
|
@@ -54,7 +68,7 @@ export async function normalizeBodyInitAsync(
|
|
|
54
68
|
return { body: new Uint8Array(body.buffer, body.byteOffset, body.byteLength) };
|
|
55
69
|
}
|
|
56
70
|
|
|
57
|
-
if (body instanceof Blob) {
|
|
71
|
+
if (body instanceof Blob || isBlob(body)) {
|
|
58
72
|
return {
|
|
59
73
|
body: new Uint8Array(await blobToArrayBufferAsync(body)),
|
|
60
74
|
overriddenHeaders: [['Content-Type', body.type]],
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { TextDecoder, TextEncoder } from 'node:util';
|
|
7
7
|
import RNFormData from 'react-native/Libraries/Network/FormData';
|
|
8
|
-
import { ReadableStream } from 'web-streams-polyfill';
|
|
8
|
+
import { ReadableStream as WebReadableStream } from 'web-streams-polyfill';
|
|
9
9
|
|
|
10
10
|
import { type NativeHeadersType } from '../NativeRequest';
|
|
11
11
|
import {
|
|
@@ -25,12 +25,12 @@ globalThis.TextEncoder ??= TextEncoder;
|
|
|
25
25
|
|
|
26
26
|
describe(convertReadableStreamToUint8ArrayAsync, () => {
|
|
27
27
|
it('should convert a readable stream to a Uint8Array', async () => {
|
|
28
|
-
const stream = new
|
|
28
|
+
const stream = new WebReadableStream<Uint8Array>({
|
|
29
29
|
start(controller) {
|
|
30
30
|
controller.enqueue(new TextEncoder().encode('Hello, world!'));
|
|
31
31
|
controller.close();
|
|
32
32
|
},
|
|
33
|
-
})
|
|
33
|
+
}) as ReadableStream<Uint8Array>;
|
|
34
34
|
|
|
35
35
|
const result = await convertReadableStreamToUint8ArrayAsync(stream);
|
|
36
36
|
const resultString = new TextDecoder().decode(result);
|
|
@@ -38,22 +38,22 @@ describe(convertReadableStreamToUint8ArrayAsync, () => {
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
it('should handle an empty readable stream', async () => {
|
|
41
|
-
const stream = new
|
|
41
|
+
const stream = new WebReadableStream({
|
|
42
42
|
start(controller) {
|
|
43
43
|
controller.close();
|
|
44
44
|
},
|
|
45
|
-
});
|
|
45
|
+
}) as ReadableStream;
|
|
46
46
|
|
|
47
47
|
const result = await convertReadableStreamToUint8ArrayAsync(stream);
|
|
48
48
|
expect(result).toEqual(new Uint8Array());
|
|
49
49
|
});
|
|
50
50
|
|
|
51
51
|
it('should handle errors in the readable stream', async () => {
|
|
52
|
-
const stream = new
|
|
52
|
+
const stream = new WebReadableStream({
|
|
53
53
|
start(controller) {
|
|
54
54
|
controller.error(new Error('Stream error'));
|
|
55
55
|
},
|
|
56
|
-
});
|
|
56
|
+
}) as ReadableStream;
|
|
57
57
|
|
|
58
58
|
await expect(convertReadableStreamToUint8ArrayAsync(stream)).rejects.toThrow('Stream error');
|
|
59
59
|
});
|
|
@@ -59,9 +59,7 @@ describe(convertFormDataAsync, () => {
|
|
|
59
59
|
it(`should convert expo-file-system FileBlob`, async () => {
|
|
60
60
|
const formData = new ExpoFormData();
|
|
61
61
|
const mockFileBlob = {
|
|
62
|
-
|
|
63
|
-
bytes: () => new Uint8Array([65, 66, 67]),
|
|
64
|
-
},
|
|
62
|
+
bytes: () => new Uint8Array([65, 66, 67]),
|
|
65
63
|
};
|
|
66
64
|
// @ts-ignore
|
|
67
65
|
formData.append('blob', mockFileBlob);
|
|
@@ -1,4 +1,33 @@
|
|
|
1
1
|
import { blobToArrayBufferAsync } from '../../utils/blobUtils';
|
|
2
|
+
import { type ExpoFormDataValue } from '../FormData';
|
|
3
|
+
|
|
4
|
+
function encodeFilename(filename: string): string {
|
|
5
|
+
return encodeURIComponent(filename.replace(/\//g, '_'));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type ExpoFormHeaders = {
|
|
9
|
+
'content-disposition': string | undefined;
|
|
10
|
+
'content-type': string | undefined;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function getFormDataPartHeaders(part: ExpoFormDataValue, name: string) {
|
|
14
|
+
const contentDisposition = 'form-data; name="' + name + '"';
|
|
15
|
+
|
|
16
|
+
const headers: ExpoFormHeaders = {
|
|
17
|
+
'content-disposition': contentDisposition,
|
|
18
|
+
'content-type': undefined,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (typeof part === 'object') {
|
|
22
|
+
if ('name' in part && typeof part.name === 'string') {
|
|
23
|
+
headers['content-disposition'] += `; filename="${encodeFilename(part.name)}"`;
|
|
24
|
+
}
|
|
25
|
+
if ('type' in part && typeof part.type === 'string') {
|
|
26
|
+
headers['content-type'] = part.type;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return headers;
|
|
30
|
+
}
|
|
2
31
|
|
|
3
32
|
/**
|
|
4
33
|
* Convert FormData to Uint8Array with a boundary
|
|
@@ -10,40 +39,40 @@ export async function convertFormDataAsync(
|
|
|
10
39
|
formData: FormData,
|
|
11
40
|
boundary: string = createBoundary()
|
|
12
41
|
): Promise<{ body: Uint8Array; boundary: string }> {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
42
|
+
if (typeof formData.entries !== 'function') {
|
|
43
|
+
// @ts-expect-error: React Native's FormData is not 100% compatible with ours
|
|
44
|
+
if (typeof formData.getParts == 'function') {
|
|
45
|
+
formData.entries = function () {
|
|
46
|
+
// @ts-expect-error
|
|
47
|
+
return formData.getParts().map((part) => {
|
|
48
|
+
if (part.string) return part.string;
|
|
49
|
+
if (part.file) return part.file;
|
|
50
|
+
if (part.blob) return part.blob;
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
} else {
|
|
54
|
+
throw new Error('Unsupported FormData implementation');
|
|
55
|
+
}
|
|
16
56
|
}
|
|
17
57
|
// @ts-expect-error: React Native's FormData is not 100% compatible with ours
|
|
18
|
-
const
|
|
58
|
+
const entries: [string, ExpoFormDataValue][] = formData.entries();
|
|
19
59
|
|
|
20
60
|
const results: (Uint8Array | string)[] = [];
|
|
21
|
-
for (const entry of
|
|
61
|
+
for (const [name, entry] of entries) {
|
|
22
62
|
results.push(`--${boundary}\r\n`);
|
|
23
|
-
for (const [headerKey, headerValue] of Object.entries(entry
|
|
24
|
-
|
|
63
|
+
for (const [headerKey, headerValue] of Object.entries(getFormDataPartHeaders(entry, name))) {
|
|
64
|
+
if (headerValue) {
|
|
65
|
+
results.push(`${headerKey}: ${headerValue}\r\n`);
|
|
66
|
+
}
|
|
25
67
|
}
|
|
26
68
|
results.push(`\r\n`);
|
|
27
|
-
if ('string'
|
|
28
|
-
results.push(entry
|
|
29
|
-
} else if (
|
|
30
|
-
results.push(entry
|
|
31
|
-
} else if ('
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// When `FormData.getParts()` is called, React Native will use the spread syntax to copy the object and lose the Blob type info.
|
|
35
|
-
// We should find the original Blob instance from the `FormData._parts` internal properties.
|
|
36
|
-
// @ts-expect-error: react-native's proprietary Blob type
|
|
37
|
-
const formDatum = formData._parts?.find(
|
|
38
|
-
([_name, value]: [name: string, value: any]) => value.data?.blobId === entry._data.blobId
|
|
39
|
-
);
|
|
40
|
-
if (formDatum == null) {
|
|
41
|
-
throw new Error('Cannot find the original Blob instance from FormData');
|
|
42
|
-
}
|
|
43
|
-
if (!(formDatum[1] instanceof Blob)) {
|
|
44
|
-
throw new Error('Unexpected value type for Blob entry in FormData');
|
|
45
|
-
}
|
|
46
|
-
results.push(new Uint8Array(await blobToArrayBufferAsync(formDatum[1])));
|
|
69
|
+
if (typeof entry === 'string') {
|
|
70
|
+
results.push(entry);
|
|
71
|
+
} else if (entry instanceof Blob) {
|
|
72
|
+
results.push(new Uint8Array(await blobToArrayBufferAsync(entry)));
|
|
73
|
+
} else if (typeof entry === 'object' && 'bytes' in entry) {
|
|
74
|
+
// @ts-expect-error: File or ExpoBlob don't extend Blob but implement the interface.
|
|
75
|
+
results.push(await entry.bytes());
|
|
47
76
|
} else {
|
|
48
77
|
throw new Error('Unsupported FormDataPart implementation');
|
|
49
78
|
}
|