expo-dev-launcher 2.1.5 → 2.2.0
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/CHANGELOG.md +18 -0
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/devlauncher/helpers/DevLauncherUpdatesHelper.kt +2 -1
- package/android/src/main/java/expo/modules/devlauncher/network/DevLauncherNetworkLogger.kt +232 -0
- package/android/src/main/java/expo/modules/devlauncher/network/DevLauncherOkHttpInterceptors.kt +57 -0
- package/expo-dev-launcher-gradle-plugin/build.gradle.kts +38 -0
- package/expo-dev-launcher-gradle-plugin/src/main/kotlin/expo/modules/devlauncher/DevLauncherPlugin.kt +101 -0
- package/expo-dev-launcher.podspec +13 -6
- package/expo-module.config.json +9 -0
- package/ios/EXDevLauncherController.m +1 -0
- package/ios/EXDevLauncherUpdatesHelper.m +2 -1
- package/ios/EXDevLauncherUtils.swift +41 -0
- package/ios/Network/EXDevLauncherNetworkLogger.swift +234 -0
- package/ios/Network/EXDevLauncherRequestLoggerProtocol.swift +217 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,24 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 2.2.0 — 2023-04-13
|
|
14
|
+
|
|
15
|
+
### 🎉 New features
|
|
16
|
+
|
|
17
|
+
- Added experimental network inspector. ([#21265](https://github.com/expo/expo/pull/21265), [#21327](https://github.com/expo/expo/pull/21327) by [@kudo](https://github.com/kudo))
|
|
18
|
+
|
|
19
|
+
### 🐛 Bug fixes
|
|
20
|
+
|
|
21
|
+
- Add missing `mimeType` when emitting network responses. ([#21676](https://github.com/expo/expo/pull/21676) by [@byCedric](https://github.com/byCedric))
|
|
22
|
+
- Add missing `Network.requestWillBeSentExtraInfo` when emitting network requests. ([#21965](https://github.com/expo/expo/pull/21965) by [@byCedric](https://github.com/byCedric))
|
|
23
|
+
- Don't require legacy manifest signature in dev clients. ([#21970](https://github.com/expo/expo/pull/21970) by [@wschurman](https://github.com/wschurman))
|
|
24
|
+
|
|
25
|
+
## 2.1.6 — 2023-03-20
|
|
26
|
+
|
|
27
|
+
### 🐛 Bug fixes
|
|
28
|
+
|
|
29
|
+
- Change arg in gradle `.execute()` call to null to inherit env variables from user's env ([#21712](https://github.com/expo/expo/pull/21712) by [@phoenixiguess](https://github.com/phoenixiguess))
|
|
30
|
+
|
|
13
31
|
## 2.1.5 — 2023-03-03
|
|
14
32
|
|
|
15
33
|
### 💡 Others
|
package/android/build.gradle
CHANGED
|
@@ -40,7 +40,7 @@ android {
|
|
|
40
40
|
minSdkVersion safeExtGet('minSdkVersion', 21)
|
|
41
41
|
targetSdkVersion safeExtGet("targetSdkVersion", 33)
|
|
42
42
|
versionCode 9
|
|
43
|
-
versionName "2.
|
|
43
|
+
versionName "2.2.0"
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
lintOptions {
|
|
@@ -180,7 +180,7 @@ def versionToNumber(major, minor, patch) {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
def getNodeModulesPackageVersion(packageName, overridePropName) {
|
|
183
|
-
def nodeModulesVersion = ["node", "-e", "console.log(require('$packageName/package.json').version);"].execute(
|
|
183
|
+
def nodeModulesVersion = ["node", "-e", "console.log(require('$packageName/package.json').version);"].execute(null, projectDir).text.trim()
|
|
184
184
|
def version = safeExtGet(overridePropName, nodeModulesVersion)
|
|
185
185
|
|
|
186
186
|
def coreVersion = version.split("-")[0]
|
|
@@ -59,6 +59,7 @@ fun createUpdatesConfigurationWithUrl(url: Uri, projectUrl: Uri, installationID:
|
|
|
59
59
|
"launchWaitMs" to 60000,
|
|
60
60
|
"checkOnLaunch" to "ALWAYS",
|
|
61
61
|
"enabled" to true,
|
|
62
|
-
"requestHeaders" to requestHeaders
|
|
62
|
+
"requestHeaders" to requestHeaders,
|
|
63
|
+
"expectsSignedManifest" to false,
|
|
63
64
|
)
|
|
64
65
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
package expo.modules.devlauncher.network
|
|
2
|
+
|
|
3
|
+
import androidx.collection.ArrayMap
|
|
4
|
+
import com.facebook.react.ReactInstanceManager
|
|
5
|
+
import com.facebook.react.bridge.Inspector
|
|
6
|
+
import com.facebook.react.common.LifecycleState
|
|
7
|
+
import com.facebook.react.devsupport.DevServerHelper
|
|
8
|
+
import com.facebook.react.devsupport.InspectorPackagerConnection
|
|
9
|
+
import expo.modules.devlauncher.DevLauncherController
|
|
10
|
+
import okhttp3.Headers
|
|
11
|
+
import okhttp3.Request
|
|
12
|
+
import okhttp3.Response
|
|
13
|
+
import okio.Buffer
|
|
14
|
+
import org.json.JSONObject
|
|
15
|
+
import java.lang.ref.WeakReference
|
|
16
|
+
import java.lang.reflect.Field
|
|
17
|
+
import java.lang.reflect.Method
|
|
18
|
+
import java.math.BigDecimal
|
|
19
|
+
import java.math.RoundingMode
|
|
20
|
+
|
|
21
|
+
class DevLauncherNetworkLogger private constructor() {
|
|
22
|
+
private var reactInstanceHashCode: Int = 0
|
|
23
|
+
private var _inspectorPackagerConnection: InspectorPackagerConnectionWrapper? = null
|
|
24
|
+
|
|
25
|
+
private val inspectorPackagerConnection: InspectorPackagerConnectionWrapper
|
|
26
|
+
get() {
|
|
27
|
+
val reactInstanceManager = DevLauncherController.instance.appHost.reactInstanceManager
|
|
28
|
+
if (reactInstanceHashCode != reactInstanceManager.hashCode()) {
|
|
29
|
+
_inspectorPackagerConnection?.clear()
|
|
30
|
+
_inspectorPackagerConnection = null
|
|
31
|
+
reactInstanceHashCode = 0
|
|
32
|
+
}
|
|
33
|
+
if (_inspectorPackagerConnection == null) {
|
|
34
|
+
_inspectorPackagerConnection = InspectorPackagerConnectionWrapper(reactInstanceManager)
|
|
35
|
+
reactInstanceHashCode = reactInstanceManager.hashCode()
|
|
36
|
+
}
|
|
37
|
+
return requireNotNull(_inspectorPackagerConnection)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns true when it is allowed to send CDP events
|
|
42
|
+
*/
|
|
43
|
+
fun shouldEmitEvents(): Boolean {
|
|
44
|
+
return DevLauncherController.wasInitialized() && DevLauncherController.instance.appHost.reactInstanceManager.lifecycleState == LifecycleState.RESUMED
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Emits CDP `Network.requestWillBeSent` and `Network.requestWillBeSentExtraInfo` events
|
|
49
|
+
*/
|
|
50
|
+
fun emitNetworkWillBeSent(request: Request, requestId: String, redirectResponse: Response?) {
|
|
51
|
+
val now = BigDecimal(System.currentTimeMillis() / 1000.0).setScale(3, RoundingMode.CEILING)
|
|
52
|
+
val requestParams = buildMap<String, Any> {
|
|
53
|
+
put("url", request.url().toString())
|
|
54
|
+
put("method", request.method())
|
|
55
|
+
put("headers", request.headers().toSingleMap())
|
|
56
|
+
val body = request.body()
|
|
57
|
+
if (body != null && body.contentLength() < MAX_BODY_SIZE) {
|
|
58
|
+
val buffer = Buffer()
|
|
59
|
+
body.writeTo(buffer)
|
|
60
|
+
put("postData", buffer.readUtf8(buffer.size.coerceAtMost(MAX_BODY_SIZE)))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
val requestWillBeSentParams = buildMap<String, Any> {
|
|
64
|
+
put("requestId", requestId)
|
|
65
|
+
put("loaderId", "")
|
|
66
|
+
put("documentURL", "mobile")
|
|
67
|
+
put("initiator", mapOf("type" to "script"))
|
|
68
|
+
put("redirectHasExtraInfo", redirectResponse != null)
|
|
69
|
+
put("request", requestParams)
|
|
70
|
+
put("referrerPolicy", "no-referrer")
|
|
71
|
+
put("type", "Fetch")
|
|
72
|
+
put("timestamp", now)
|
|
73
|
+
put("wallTime", now)
|
|
74
|
+
if (redirectResponse != null) {
|
|
75
|
+
put("redirectResponse", mapOf(
|
|
76
|
+
"url" to redirectResponse.request().url().toString(),
|
|
77
|
+
"status" to redirectResponse.code(),
|
|
78
|
+
"statusText" to redirectResponse.message(),
|
|
79
|
+
"headers" to redirectResponse.headers().toSingleMap(),
|
|
80
|
+
))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
val requestWillBeSentData = JSONObject(mapOf(
|
|
84
|
+
"method" to "Network.requestWillBeSent",
|
|
85
|
+
"params" to requestWillBeSentParams,
|
|
86
|
+
))
|
|
87
|
+
inspectorPackagerConnection.sendWrappedEventToAllPages(requestWillBeSentData.toString())
|
|
88
|
+
|
|
89
|
+
val extraInfoParams = mapOf(
|
|
90
|
+
"requestId" to requestId,
|
|
91
|
+
"associatedCookies" to emptyList<Void>(),
|
|
92
|
+
"headers" to request.headers().toSingleMap(),
|
|
93
|
+
"connectTiming" to mapOf(
|
|
94
|
+
"requestTime" to now,
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
val extraInfoData = JSONObject(mapOf(
|
|
98
|
+
"method" to "Network.requestWillBeSentExtraInfo",
|
|
99
|
+
"params" to extraInfoParams
|
|
100
|
+
))
|
|
101
|
+
inspectorPackagerConnection.sendWrappedEventToAllPages(extraInfoData.toString())
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Emits CDP `Network.responseReceived` and `Network.loadingFinished` events
|
|
106
|
+
*/
|
|
107
|
+
fun emitNetworkResponse(request: Request, requestId: String, response: Response) {
|
|
108
|
+
val now = BigDecimal(System.currentTimeMillis() / 1000.0).setScale(3, RoundingMode.CEILING)
|
|
109
|
+
val responseReceivedParams = mapOf(
|
|
110
|
+
"requestId" to requestId,
|
|
111
|
+
"loaderId" to "",
|
|
112
|
+
"hasExtraInfo" to false,
|
|
113
|
+
"response" to mapOf(
|
|
114
|
+
"url" to request.url().toString(),
|
|
115
|
+
"status" to response.code(),
|
|
116
|
+
"statusText" to response.message(),
|
|
117
|
+
"headers" to response.headers().toSingleMap(),
|
|
118
|
+
"mimeType" to response.header("Content-Type", ""),
|
|
119
|
+
),
|
|
120
|
+
"referrerPolicy" to "no-referrer",
|
|
121
|
+
"type" to "Fetch",
|
|
122
|
+
"timestamp" to now,
|
|
123
|
+
)
|
|
124
|
+
val responseReceivedData = JSONObject(mapOf(
|
|
125
|
+
"method" to "Network.responseReceived",
|
|
126
|
+
"params" to responseReceivedParams,
|
|
127
|
+
))
|
|
128
|
+
inspectorPackagerConnection.sendWrappedEventToAllPages(responseReceivedData.toString())
|
|
129
|
+
|
|
130
|
+
val loadingFinishedParams = mapOf(
|
|
131
|
+
"requestId" to requestId,
|
|
132
|
+
"timestamp" to now,
|
|
133
|
+
"encodedDataLength" to (response.body()?.contentLength() ?: 0),
|
|
134
|
+
)
|
|
135
|
+
val loadingFinishedData = JSONObject(mapOf(
|
|
136
|
+
"method" to "Network.loadingFinished",
|
|
137
|
+
"params" to loadingFinishedParams,
|
|
138
|
+
))
|
|
139
|
+
inspectorPackagerConnection.sendWrappedEventToAllPages(loadingFinishedData.toString())
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Emits our custom `Expo(Network.receivedResponseBody)` event
|
|
144
|
+
*/
|
|
145
|
+
fun emitNetworkDidReceiveBody(requestId: String, response: Response) {
|
|
146
|
+
val contentLength = response.body()?.contentLength() ?: 0
|
|
147
|
+
if (contentLength <= 0 || contentLength > MAX_BODY_SIZE) {
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
val body = response.peekBody(MAX_BODY_SIZE)
|
|
151
|
+
val contentType = body.contentType()
|
|
152
|
+
val isText = contentType?.type() == "text" || (contentType?.type() == "application" && contentType?.subtype() == "json")
|
|
153
|
+
val bodyString = if (isText) body.string() else body.source().readByteString().base64()
|
|
154
|
+
val params = mapOf(
|
|
155
|
+
"requestId" to requestId,
|
|
156
|
+
"body" to bodyString,
|
|
157
|
+
"base64Encoded" to !isText,
|
|
158
|
+
)
|
|
159
|
+
val data = JSONObject(mapOf(
|
|
160
|
+
"method" to "Expo(Network.receivedResponseBody)",
|
|
161
|
+
"params" to params,
|
|
162
|
+
))
|
|
163
|
+
inspectorPackagerConnection.sendWrappedEventToAllPages(data.toString())
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
companion object {
|
|
167
|
+
val instance = DevLauncherNetworkLogger()
|
|
168
|
+
private const val MAX_BODY_SIZE = 1048576L
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* A `InspectorPackagerConnection` wrapper to expose private members with reflection
|
|
174
|
+
*/
|
|
175
|
+
internal class InspectorPackagerConnectionWrapper constructor(reactInstanceManager: ReactInstanceManager) {
|
|
176
|
+
private var inspectorPackagerConnectionWeak: WeakReference<InspectorPackagerConnection> = WeakReference(null)
|
|
177
|
+
private val devServerHelperWeak: WeakReference<DevServerHelper>
|
|
178
|
+
private val inspectorPackagerConnectionField: Field
|
|
179
|
+
private val sendWrappedEventMethod: Method
|
|
180
|
+
|
|
181
|
+
private val inspectorPackagerConnection: InspectorPackagerConnection?
|
|
182
|
+
get() {
|
|
183
|
+
var inspectorPackagerConnection = inspectorPackagerConnectionWeak.get()
|
|
184
|
+
if (inspectorPackagerConnection == null) {
|
|
185
|
+
val devServerHelper = devServerHelperWeak.get() ?: return null
|
|
186
|
+
inspectorPackagerConnection = inspectorPackagerConnectionField[devServerHelper] as? InspectorPackagerConnection
|
|
187
|
+
|
|
188
|
+
if (inspectorPackagerConnection != null) {
|
|
189
|
+
inspectorPackagerConnectionWeak = WeakReference(inspectorPackagerConnection)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return inspectorPackagerConnection
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
init {
|
|
196
|
+
val devSupportManager = reactInstanceManager.devSupportManager
|
|
197
|
+
val devSupportManagerBaseClass: Class<*> = devSupportManager.javaClass.superclass
|
|
198
|
+
val mDevServerHelperField = devSupportManagerBaseClass.getDeclaredField("mDevServerHelper")
|
|
199
|
+
mDevServerHelperField.isAccessible = true
|
|
200
|
+
val devServerHelper = mDevServerHelperField[devSupportManager]
|
|
201
|
+
devServerHelperWeak = WeakReference(devServerHelper as DevServerHelper)
|
|
202
|
+
|
|
203
|
+
inspectorPackagerConnectionField = DevServerHelper::class.java.getDeclaredField("mInspectorPackagerConnection")
|
|
204
|
+
inspectorPackagerConnectionField.isAccessible = true
|
|
205
|
+
|
|
206
|
+
sendWrappedEventMethod = InspectorPackagerConnection::class.java.getDeclaredMethod("sendWrappedEvent", String::class.java, String::class.java)
|
|
207
|
+
sendWrappedEventMethod.isAccessible = true
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fun clear() {
|
|
211
|
+
inspectorPackagerConnectionWeak.clear()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
fun sendWrappedEventToAllPages(event: String) {
|
|
215
|
+
val inspectorPackagerConnection = this.inspectorPackagerConnection ?: return
|
|
216
|
+
for (page in Inspector.getPages()) {
|
|
217
|
+
sendWrappedEventMethod.invoke(inspectorPackagerConnection, page.id.toString(), event)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* OkHttp `Headers` extension method to generate a simple key-value map
|
|
224
|
+
* which only exposing single value for a key.
|
|
225
|
+
*/
|
|
226
|
+
fun Headers.toSingleMap(): Map<String, String> {
|
|
227
|
+
val result = ArrayMap<String, String>()
|
|
228
|
+
for (key in names()) {
|
|
229
|
+
result[key] = get(key)
|
|
230
|
+
}
|
|
231
|
+
return result
|
|
232
|
+
}
|
package/android/src/main/java/expo/modules/devlauncher/network/DevLauncherOkHttpInterceptors.kt
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package expo.modules.devlauncher.network
|
|
2
|
+
|
|
3
|
+
import okhttp3.Interceptor
|
|
4
|
+
import okhttp3.Response
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The OkHttp network interceptor to log requests and the CDP events to [DevLauncherNetworkLogger]
|
|
8
|
+
*/
|
|
9
|
+
@Suppress("unused")
|
|
10
|
+
class DevLauncherOkHttpNetworkInterceptor : Interceptor {
|
|
11
|
+
override fun intercept(chain: Interceptor.Chain): Response {
|
|
12
|
+
if (!DevLauncherNetworkLogger.instance.shouldEmitEvents()) {
|
|
13
|
+
return chain.proceed(chain.request());
|
|
14
|
+
}
|
|
15
|
+
val request = chain.request()
|
|
16
|
+
val redirectResponse = request.tag(RedirectResponse::class.java)
|
|
17
|
+
val requestId = redirectResponse?.requestId ?: request.hashCode().toString()
|
|
18
|
+
DevLauncherNetworkLogger.instance.emitNetworkWillBeSent(request, requestId, redirectResponse?.priorResponse)
|
|
19
|
+
|
|
20
|
+
val response = chain.proceed(request)
|
|
21
|
+
|
|
22
|
+
if (response.isRedirect) {
|
|
23
|
+
response.request().tag(RedirectResponse::class.java)?.let {
|
|
24
|
+
it.requestId = requestId
|
|
25
|
+
it.priorResponse = response
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
DevLauncherNetworkLogger.instance.emitNetworkResponse(request, requestId, response)
|
|
29
|
+
DevLauncherNetworkLogger.instance.emitNetworkDidReceiveBody(requestId, response)
|
|
30
|
+
}
|
|
31
|
+
return response
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The OkHttp app interceptor to add custom tag for [RedirectResponse]
|
|
37
|
+
*/
|
|
38
|
+
@Suppress("unused")
|
|
39
|
+
class DevLauncherOkHttpAppInterceptor : Interceptor {
|
|
40
|
+
override fun intercept(chain: Interceptor.Chain): Response {
|
|
41
|
+
if (!DevLauncherNetworkLogger.instance.shouldEmitEvents()) {
|
|
42
|
+
return chain.proceed(chain.request());
|
|
43
|
+
}
|
|
44
|
+
return chain.proceed(chain.request().newBuilder()
|
|
45
|
+
.tag(RedirectResponse::class.java, RedirectResponse())
|
|
46
|
+
.build()
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Custom property for redirect requests
|
|
53
|
+
*/
|
|
54
|
+
internal class RedirectResponse {
|
|
55
|
+
var requestId: String? = null
|
|
56
|
+
var priorResponse: Response? = null
|
|
57
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|
2
|
+
|
|
3
|
+
plugins {
|
|
4
|
+
kotlin("jvm") version "1.8.10"
|
|
5
|
+
id("java-gradle-plugin")
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
repositories {
|
|
9
|
+
google()
|
|
10
|
+
mavenCentral()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
dependencies {
|
|
14
|
+
implementation(gradleApi())
|
|
15
|
+
implementation("com.android.tools.build:gradle:7.3.1")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
java {
|
|
19
|
+
sourceCompatibility = JavaVersion.VERSION_11
|
|
20
|
+
targetCompatibility = JavaVersion.VERSION_11
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
tasks.withType<KotlinCompile> {
|
|
24
|
+
kotlinOptions {
|
|
25
|
+
jvmTarget = JavaVersion.VERSION_11.toString()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
group = "expo.modules"
|
|
30
|
+
|
|
31
|
+
gradlePlugin {
|
|
32
|
+
plugins {
|
|
33
|
+
register("expoDevLauncherPlugin") {
|
|
34
|
+
id = "expo-dev-launcher-gradle-plugin"
|
|
35
|
+
implementationClass = "expo.modules.devlauncher.DevLauncherPlugin"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
package expo.modules.devlauncher
|
|
2
|
+
|
|
3
|
+
import com.android.build.api.instrumentation.AsmClassVisitorFactory
|
|
4
|
+
import com.android.build.api.instrumentation.ClassContext
|
|
5
|
+
import com.android.build.api.instrumentation.ClassData
|
|
6
|
+
import com.android.build.api.instrumentation.FramesComputationMode
|
|
7
|
+
import com.android.build.api.instrumentation.InstrumentationParameters
|
|
8
|
+
import com.android.build.api.instrumentation.InstrumentationScope
|
|
9
|
+
import com.android.build.api.variant.AndroidComponentsExtension
|
|
10
|
+
import org.gradle.api.Plugin
|
|
11
|
+
import org.gradle.api.Project
|
|
12
|
+
import org.gradle.api.provider.Property
|
|
13
|
+
import org.gradle.api.tasks.Input
|
|
14
|
+
import org.gradle.api.tasks.Optional
|
|
15
|
+
import org.objectweb.asm.ClassVisitor
|
|
16
|
+
import org.objectweb.asm.MethodVisitor
|
|
17
|
+
import org.objectweb.asm.Opcodes
|
|
18
|
+
import org.slf4j.LoggerFactory
|
|
19
|
+
|
|
20
|
+
abstract class DevLauncherPlugin : Plugin<Project> {
|
|
21
|
+
|
|
22
|
+
override fun apply(project: Project) {
|
|
23
|
+
val enableNetworkInspector = project.properties["EX_DEV_CLIENT_NETWORK_INSPECTOR"]?.toString()?.toBoolean()
|
|
24
|
+
if (enableNetworkInspector != null && enableNetworkInspector) {
|
|
25
|
+
val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
|
|
26
|
+
androidComponents.onVariants(androidComponents.selector().withBuildType("debug")) { variant ->
|
|
27
|
+
variant.instrumentation.transformClassesWith(DevLauncherClassVisitorFactory::class.java, InstrumentationScope.ALL) {
|
|
28
|
+
}
|
|
29
|
+
variant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface DevLauncherPluginParameters : InstrumentationParameters {
|
|
35
|
+
@get:Input
|
|
36
|
+
@get:Optional
|
|
37
|
+
val enabled: Property<Boolean>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
abstract class DevLauncherClassVisitorFactory : AsmClassVisitorFactory<DevLauncherPluginParameters> {
|
|
41
|
+
override fun createClassVisitor(
|
|
42
|
+
classContext: ClassContext,
|
|
43
|
+
nextClassVisitor: ClassVisitor
|
|
44
|
+
): ClassVisitor {
|
|
45
|
+
if (parameters.get().enabled.getOrElse(false)) {
|
|
46
|
+
return nextClassVisitor
|
|
47
|
+
}
|
|
48
|
+
return OkHttpClassVisitor(classContext, instrumentationContext.apiVersion.get(), nextClassVisitor)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
override fun isInstrumentable(classData: ClassData): Boolean {
|
|
52
|
+
if (parameters.get().enabled.getOrElse(false)) {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
return classData.className in listOf("okhttp3.OkHttpClient\$Builder")
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class OkHttpClassVisitor(private val classContext: ClassContext, api: Int, classVisitor: ClassVisitor) : ClassVisitor(api, classVisitor) {
|
|
60
|
+
override fun visitMethod(access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor {
|
|
61
|
+
val originalVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
|
|
62
|
+
if (name == "build") {
|
|
63
|
+
return OkHttpClientCustomBuildMethod(api, originalVisitor)
|
|
64
|
+
}
|
|
65
|
+
return originalVisitor
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class OkHttpClientCustomBuildMethod(api: Int, methodVisitor: MethodVisitor) : MethodVisitor(api, methodVisitor) {
|
|
70
|
+
override fun visitCode() {
|
|
71
|
+
// opcodes for `this.addInterceptor(expo.modules.devlauncher.network.DevLauncherOkHttpAppInterceptor())`
|
|
72
|
+
visitVarInsn(Opcodes.ALOAD, 0)
|
|
73
|
+
visitTypeInsn(Opcodes.NEW, "expo/modules/devlauncher/network/DevLauncherOkHttpAppInterceptor")
|
|
74
|
+
visitInsn(Opcodes.DUP)
|
|
75
|
+
visitMethodInsn(Opcodes.INVOKESPECIAL, "expo/modules/devlauncher/network/DevLauncherOkHttpAppInterceptor", "<init>", "()V", false)
|
|
76
|
+
visitTypeInsn(Opcodes.CHECKCAST, "okhttp3/Interceptor")
|
|
77
|
+
visitMethodInsn(Opcodes.INVOKEVIRTUAL, "okhttp3/OkHttpClient\$Builder", "addInterceptor", "(Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient\$Builder;", false)
|
|
78
|
+
|
|
79
|
+
// opcodes for `this.addNetworkInterceptor(expo.modules.devlauncher.network.DevLauncherOkHttpNetworkInterceptor())`
|
|
80
|
+
visitVarInsn(Opcodes.ALOAD, 0)
|
|
81
|
+
visitTypeInsn(Opcodes.NEW, "expo/modules/devlauncher/network/DevLauncherOkHttpNetworkInterceptor")
|
|
82
|
+
visitInsn(Opcodes.DUP)
|
|
83
|
+
visitMethodInsn(Opcodes.INVOKESPECIAL, "expo/modules/devlauncher/network/DevLauncherOkHttpNetworkInterceptor", "<init>", "()V", false)
|
|
84
|
+
visitTypeInsn(Opcodes.CHECKCAST, "okhttp3/Interceptor")
|
|
85
|
+
visitMethodInsn(Opcodes.INVOKEVIRTUAL, "okhttp3/OkHttpClient\$Builder", "addNetworkInterceptor", "(Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient\$Builder;", false)
|
|
86
|
+
|
|
87
|
+
// opcodes for `return OkHttpClient(this)`
|
|
88
|
+
visitTypeInsn(Opcodes.NEW, "okhttp3/OkHttpClient")
|
|
89
|
+
visitInsn(Opcodes.DUP)
|
|
90
|
+
visitVarInsn(Opcodes.ALOAD, 0)
|
|
91
|
+
visitMethodInsn(Opcodes.INVOKESPECIAL, "okhttp3/OkHttpClient", "<init>", "(Lokhttp3/OkHttpClient\$Builder;)V", false)
|
|
92
|
+
visitInsn(Opcodes.ARETURN)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
companion object {
|
|
97
|
+
internal val logger by lazy {
|
|
98
|
+
LoggerFactory.getLogger(DevLauncherPlugin::class.java)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -32,17 +32,24 @@ Pod::Spec.new do |s|
|
|
|
32
32
|
'GCC_PREPROCESSOR_DEFINITIONS' => "EX_DEV_LAUNCHER_VERSION=#{s.version}"
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
s.pod_target_xcconfig = { "DEFINES_MODULE" => "YES" }
|
|
35
|
+
other_c_flags = '$(inherited)'
|
|
37
36
|
dev_launcher_url = ENV['EX_DEV_LAUNCHER_URL'] || ""
|
|
38
37
|
if dev_launcher_url != ""
|
|
39
38
|
escaped_dev_launcher_url = Shellwords.escape(dev_launcher_url).gsub('/','\\/')
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
other_c_flags += " -DEX_DEV_LAUNCHER_URL=\"\\\"" + escaped_dev_launcher_url + "\\\"\""
|
|
40
|
+
end
|
|
41
|
+
other_swift_flags = "$(inherited)"
|
|
42
|
+
if ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] == '1'
|
|
43
|
+
other_swift_flags += ' -DEX_DEV_CLIENT_NETWORK_INSPECTOR'
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
# Swift/Objective-C compatibility
|
|
47
|
+
s.pod_target_xcconfig = {
|
|
48
|
+
'DEFINES_MODULE' => 'YES',
|
|
49
|
+
'OTHER_CFLAGS[config=Debug]' => other_c_flags,
|
|
50
|
+
'OTHER_SWIFT_FLAGS[config=Debug]' => other_swift_flags,
|
|
51
|
+
}
|
|
52
|
+
|
|
46
53
|
s.user_target_xcconfig = {
|
|
47
54
|
"HEADER_SEARCH_PATHS" => "\"${PODS_CONFIGURATION_BUILD_DIR}/expo-dev-launcher/Swift Compatibility Header\"",
|
|
48
55
|
}
|
package/expo-module.config.json
CHANGED
|
@@ -7,5 +7,14 @@
|
|
|
7
7
|
"appDelegateSubscribers": ["ExpoDevLauncherAppDelegateSubscriber"],
|
|
8
8
|
"reactDelegateHandlers": ["ExpoDevLauncherReactDelegateHandler"],
|
|
9
9
|
"debugOnly": true
|
|
10
|
+
},
|
|
11
|
+
"android": {
|
|
12
|
+
"gradlePlugins": [
|
|
13
|
+
{
|
|
14
|
+
"id": "expo-dev-launcher-gradle-plugin",
|
|
15
|
+
"group": "expo.modules",
|
|
16
|
+
"sourceDir": "expo-dev-launcher-gradle-plugin"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
10
19
|
}
|
|
11
20
|
}
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
self.errorManager = [[EXDevLauncherErrorManager alloc] initWithController:self];
|
|
79
79
|
self.installationIDHelper = [EXDevLauncherInstallationIDHelper new];
|
|
80
80
|
self.shouldPreferUpdatesInterfaceSourceUrl = NO;
|
|
81
|
+
[EXDevLauncherNetworkLogger.shared enable];
|
|
81
82
|
}
|
|
82
83
|
return self;
|
|
83
84
|
}
|
|
@@ -22,7 +22,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
22
22
|
@"EXUpdatesCheckOnLaunch": @"ALWAYS",
|
|
23
23
|
@"EXUpdatesHasEmbeddedUpdate": @(NO),
|
|
24
24
|
@"EXUpdatesEnabled": @(YES),
|
|
25
|
-
@"EXUpdatesRequestHeaders": requestHeaders
|
|
25
|
+
@"EXUpdatesRequestHeaders": requestHeaders,
|
|
26
|
+
@"EXUpdatesExpectsSignedManifest": @(NO),
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -23,6 +23,47 @@ class EXDevLauncherUtils {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
Swizzles implementations of given class method selectors.
|
|
28
|
+
This function will backup original selector implementation for `invokeOriginalClassMethod`.
|
|
29
|
+
*/
|
|
30
|
+
static func swizzleClassMethod(selector selectorA: Selector, withSelector selectorB: Selector, forClass: AnyClass) {
|
|
31
|
+
if let methodA = class_getClassMethod(forClass, selectorA),
|
|
32
|
+
let methodB = class_getClassMethod(forClass, selectorB) {
|
|
33
|
+
let impA = method_getImplementation(methodA)
|
|
34
|
+
let backupSelectorA = NSSelectorFromString("_" + NSStringFromSelector(selectorA))
|
|
35
|
+
let metaClass = objc_getMetaClass(String(describing: forClass)) as? AnyClass
|
|
36
|
+
class_addMethod(metaClass, backupSelectorA, impA, method_getTypeEncoding(methodA))
|
|
37
|
+
method_setImplementation(methodA, method_getImplementation(methodB))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
Invokes the original implementation before swizzling for the given selector
|
|
43
|
+
*/
|
|
44
|
+
static func invokeOriginalClassMethod(selector: Selector, forClass: AnyClass) throws -> Any? {
|
|
45
|
+
typealias ClassMethod = @convention(c) (AnyObject, Selector) -> Any
|
|
46
|
+
let imp = try getOriginalClassMethodImp(selector: selector, forClass: forClass)
|
|
47
|
+
return unsafeBitCast(imp, to: ClassMethod.self)(self, selector)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
Invokes the original implementation before swizzling for the given selector
|
|
52
|
+
*/
|
|
53
|
+
static func invokeOriginalClassMethod(selector: Selector, forClass: AnyClass, A0: Any) throws -> Any? {
|
|
54
|
+
typealias ClassMethod = @convention(c) (AnyObject, Selector, Any) -> Any
|
|
55
|
+
let imp = try getOriginalClassMethodImp(selector: selector, forClass: forClass)
|
|
56
|
+
return unsafeBitCast(imp, to: ClassMethod.self)(self, selector, A0)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private static func getOriginalClassMethodImp(selector: Selector, forClass: AnyClass) throws -> IMP {
|
|
60
|
+
let backupSelector = NSSelectorFromString("_" + NSStringFromSelector(selector))
|
|
61
|
+
guard let method = class_getClassMethod(forClass, backupSelector) else {
|
|
62
|
+
fatalError("Backup selector does not exist - forClass[\(forClass)] backupSelector[\(NSStringFromSelector(backupSelector))]")
|
|
63
|
+
}
|
|
64
|
+
return method_getImplementation(method)
|
|
65
|
+
}
|
|
66
|
+
|
|
26
67
|
static func resourcesBundle() -> Bundle? {
|
|
27
68
|
let frameworkBundle = Bundle(for: EXDevLauncherUtils.self)
|
|
28
69
|
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import React
|
|
2
|
+
|
|
3
|
+
#if DEBUG && EX_DEV_CLIENT_NETWORK_INSPECTOR
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
This class intercepts all default `URLSession` requests and send CDP events to the connecting metro-inspector-proxy
|
|
7
|
+
*/
|
|
8
|
+
@objc
|
|
9
|
+
public class EXDevLauncherNetworkLogger: NSObject {
|
|
10
|
+
private var enabled: Bool = false
|
|
11
|
+
internal var inspectorPackagerConn: RCTInspectorPackagerConnection?
|
|
12
|
+
|
|
13
|
+
@objc
|
|
14
|
+
public static let shared = EXDevLauncherNetworkLogger()
|
|
15
|
+
|
|
16
|
+
override private init() {}
|
|
17
|
+
|
|
18
|
+
@objc
|
|
19
|
+
public func enable() {
|
|
20
|
+
EXDevLauncherUtils.swizzleClassMethod(
|
|
21
|
+
selector: #selector(RCTInspectorDevServerHelper.connect(withBundleURL:)),
|
|
22
|
+
withSelector: #selector(RCTInspectorDevServerHelper.EXDevLauncher_connect(withBundleURL:)),
|
|
23
|
+
forClass: RCTInspectorDevServerHelper.self
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
EXDevLauncherUtils.swizzleClassMethod(
|
|
27
|
+
selector: #selector(getter: URLSessionConfiguration.default),
|
|
28
|
+
withSelector: #selector(URLSessionConfiguration.EXDevLauncher_urlSessionConfiguration),
|
|
29
|
+
forClass: URLSessionConfiguration.self
|
|
30
|
+
)
|
|
31
|
+
enabled = true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
Emits CDP `Network.requestWillBeSent` and `Network.requestWillBeSentExtraInfo` events
|
|
36
|
+
*/
|
|
37
|
+
func emitNetworkWillBeSent(request: URLRequest, requestId: String, redirectResponse: HTTPURLResponse?) {
|
|
38
|
+
let now = Date().timeIntervalSince1970
|
|
39
|
+
var requestParams: [String: Any] = [
|
|
40
|
+
"url": request.url?.absoluteString,
|
|
41
|
+
"method": request.httpMethod,
|
|
42
|
+
"headers": request.allHTTPHeaderFields
|
|
43
|
+
]
|
|
44
|
+
if let httpBody = request.httpBodyData() {
|
|
45
|
+
requestParams["postData"] = String(data: httpBody, encoding: .utf8)
|
|
46
|
+
}
|
|
47
|
+
var params = [
|
|
48
|
+
"requestId": requestId,
|
|
49
|
+
"loaderId": "",
|
|
50
|
+
"documentURL": "mobile",
|
|
51
|
+
"initiator": ["type": "script"],
|
|
52
|
+
"redirectHasExtraInfo": redirectResponse != nil,
|
|
53
|
+
"request": requestParams,
|
|
54
|
+
"referrerPolicy": "no-referrer",
|
|
55
|
+
"type": "Fetch",
|
|
56
|
+
"timestamp": now,
|
|
57
|
+
"wallTime": now
|
|
58
|
+
] as [String: Any]
|
|
59
|
+
if let redirectResponse {
|
|
60
|
+
params["redirectResponse"] = [
|
|
61
|
+
"url": redirectResponse.url?.absoluteString,
|
|
62
|
+
"status": redirectResponse.statusCode,
|
|
63
|
+
"statusText": "",
|
|
64
|
+
"headers": redirectResponse.allHeaderFields
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
if let data = try? JSONSerialization.data(
|
|
68
|
+
withJSONObject: ["method": "Network.requestWillBeSent", "params": params],
|
|
69
|
+
options: []
|
|
70
|
+
), let message = String(data: data, encoding: .utf8) {
|
|
71
|
+
inspectorPackagerConn?.sendWrappedEventToAllPages(message)
|
|
72
|
+
}
|
|
73
|
+
params = [
|
|
74
|
+
"requestId": requestId,
|
|
75
|
+
"associatedCookies": [],
|
|
76
|
+
"headers": requestParams["headers"],
|
|
77
|
+
"connectTiming": [
|
|
78
|
+
"requestTime": now
|
|
79
|
+
]
|
|
80
|
+
] as [String: Any]
|
|
81
|
+
if let data = try? JSONSerialization.data(
|
|
82
|
+
withJSONObject: ["method": "Network.requestWillBeSentExtraInfo", "params": params],
|
|
83
|
+
options: []
|
|
84
|
+
), let message = String(data: data, encoding: .utf8) {
|
|
85
|
+
inspectorPackagerConn?.sendWrappedEventToAllPages(message)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
Emits CDP `Network.responseReceived` and `Network.loadingFinished` events
|
|
91
|
+
*/
|
|
92
|
+
func emitNetworkResponse(request: URLRequest, requestId: String, response: HTTPURLResponse) {
|
|
93
|
+
let now = Date().timeIntervalSince1970
|
|
94
|
+
|
|
95
|
+
var params = [
|
|
96
|
+
"requestId": requestId,
|
|
97
|
+
"loaderId": "",
|
|
98
|
+
"hasExtraInfo": false,
|
|
99
|
+
"response": [
|
|
100
|
+
"url": request.url?.absoluteString,
|
|
101
|
+
"status": response.statusCode,
|
|
102
|
+
"statusText": "",
|
|
103
|
+
"headers": response.allHeaderFields,
|
|
104
|
+
"mimeType": response.value(forHTTPHeaderField: "Content-Type") ?? ""
|
|
105
|
+
],
|
|
106
|
+
"referrerPolicy": "no-referrer",
|
|
107
|
+
"type": "Fetch",
|
|
108
|
+
"timestamp": now
|
|
109
|
+
] as [String: Any]
|
|
110
|
+
if let data = try? JSONSerialization.data(
|
|
111
|
+
withJSONObject: ["method": "Network.responseReceived", "params": params],
|
|
112
|
+
options: []
|
|
113
|
+
), let message = String(data: data, encoding: .utf8) {
|
|
114
|
+
inspectorPackagerConn?.sendWrappedEventToAllPages(message)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
params = [
|
|
118
|
+
"requestId": requestId,
|
|
119
|
+
"timestamp": now,
|
|
120
|
+
"encodedDataLength": response.expectedContentLength
|
|
121
|
+
] as [String: Any]
|
|
122
|
+
if let data = try? JSONSerialization.data(
|
|
123
|
+
withJSONObject: [
|
|
124
|
+
"method": "Network.loadingFinished",
|
|
125
|
+
"params": params
|
|
126
|
+
],
|
|
127
|
+
options: []
|
|
128
|
+
), let message = String(data: data, encoding: .utf8) {
|
|
129
|
+
inspectorPackagerConn?.sendWrappedEventToAllPages(message)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
Emits our custom `Expo(Network.receivedResponseBody)` event
|
|
135
|
+
*/
|
|
136
|
+
func emitNetworkDidReceiveBody(requestId: String, responseBody: Data, isText: Bool) {
|
|
137
|
+
let bodyString = isText
|
|
138
|
+
? String(data: responseBody, encoding: .utf8)
|
|
139
|
+
: responseBody.base64EncodedString()
|
|
140
|
+
let params = [
|
|
141
|
+
"requestId": requestId,
|
|
142
|
+
"body": bodyString,
|
|
143
|
+
"base64Encoded": !isText
|
|
144
|
+
] as [String: Any]
|
|
145
|
+
if let data = try? JSONSerialization.data(
|
|
146
|
+
withJSONObject: [
|
|
147
|
+
"method": "Expo(Network.receivedResponseBody)",
|
|
148
|
+
"params": params
|
|
149
|
+
],
|
|
150
|
+
options: []
|
|
151
|
+
), let message = String(data: data, encoding: .utf8) {
|
|
152
|
+
inspectorPackagerConn?.sendWrappedEventToAllPages(message)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
extension URLSessionConfiguration {
|
|
158
|
+
private typealias GetterFunc = @convention(c) (AnyObject, Selector) -> URLSessionConfiguration
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
Swizzled `URLSessionConfiguration.default` for us to add the `EXDevLauncherRequestLoggerProtocol` interceptor
|
|
162
|
+
*/
|
|
163
|
+
@objc
|
|
164
|
+
static func EXDevLauncher_urlSessionConfiguration() -> URLSessionConfiguration {
|
|
165
|
+
guard let config = try? EXDevLauncherUtils.invokeOriginalClassMethod(
|
|
166
|
+
selector: #selector(getter: URLSessionConfiguration.default),
|
|
167
|
+
forClass: URLSessionConfiguration.self
|
|
168
|
+
) as? URLSessionConfiguration else {
|
|
169
|
+
fatalError("Unable to get original URLSessionConfiguration.default")
|
|
170
|
+
}
|
|
171
|
+
var protocolClasses = config.protocolClasses
|
|
172
|
+
protocolClasses?.insert(EXDevLauncherRequestLoggerProtocol.self, at: 0)
|
|
173
|
+
config.protocolClasses = protocolClasses
|
|
174
|
+
return config
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
extension RCTInspectorDevServerHelper {
|
|
179
|
+
private typealias ConnectFunc = @convention(c) (AnyObject, Selector, URL)
|
|
180
|
+
-> RCTInspectorPackagerConnection?
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
Swizzled `RCTInspectorDevServerHelper.connect(withBundleURL:)` for us to get the `RCTInspectorPackagerConnection` instance
|
|
184
|
+
*/
|
|
185
|
+
@objc
|
|
186
|
+
static func EXDevLauncher_connect(withBundleURL bundleURL: URL)
|
|
187
|
+
-> RCTInspectorPackagerConnection? {
|
|
188
|
+
let inspectorPackagerConn = try? EXDevLauncherUtils.invokeOriginalClassMethod(
|
|
189
|
+
selector: #selector(RCTInspectorDevServerHelper.connect(withBundleURL:)),
|
|
190
|
+
forClass: RCTInspectorDevServerHelper.self,
|
|
191
|
+
A0: bundleURL
|
|
192
|
+
) as? RCTInspectorPackagerConnection
|
|
193
|
+
|
|
194
|
+
EXDevLauncherNetworkLogger.shared.inspectorPackagerConn = inspectorPackagerConn
|
|
195
|
+
return inspectorPackagerConn
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
extension RCTInspectorPackagerConnection {
|
|
200
|
+
/**
|
|
201
|
+
Sends message from native to inspector proxy
|
|
202
|
+
*/
|
|
203
|
+
func sendWrappedEventToAllPages(_ event: String) {
|
|
204
|
+
for page in RCTInspector.pages() {
|
|
205
|
+
perform(NSSelectorFromString("sendWrappedEvent:message:"), with: String(page.id), with: event)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#else
|
|
211
|
+
|
|
212
|
+
@objc
|
|
213
|
+
public class EXDevLauncherNetworkLogger: NSObject {
|
|
214
|
+
@objc
|
|
215
|
+
public static let shared = EXDevLauncherNetworkLogger()
|
|
216
|
+
|
|
217
|
+
override private init() {}
|
|
218
|
+
|
|
219
|
+
@objc
|
|
220
|
+
public func enable() {
|
|
221
|
+
// no-op when running on release build where RCTInspector classes not exported
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
func emitNetworkWillBeSent(request: URLRequest, requestId: String, redirectResponse: HTTPURLResponse?) {
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
func emitNetworkResponse(request: URLRequest, requestId: String, response: HTTPURLResponse) {
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
func emitNetworkDidReceiveBody(requestId: String, responseBody: Data, isText: Bool) {
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#endif
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
A `URLSession` interceptor to log requests and send events to the `EXDevLauncherNetworkLogger`
|
|
3
|
+
*/
|
|
4
|
+
@objc
|
|
5
|
+
class EXDevLauncherRequestLoggerProtocol: URLProtocol, URLSessionDataDelegate {
|
|
6
|
+
private static let REQUEST_ID = "EXDevLauncherRequestLoggerProtocol.requestId"
|
|
7
|
+
private static let REDIRECT_RESPONSE = "EXDevLauncherRequestLoggerProtocol.redirectResponse"
|
|
8
|
+
static let MAX_BODY_SIZE = 1_048_576
|
|
9
|
+
private static var requestIdProvider = RequestIdProvider()
|
|
10
|
+
private lazy var urlSession = URLSession(
|
|
11
|
+
configuration: URLSessionConfiguration.default,
|
|
12
|
+
delegate: self,
|
|
13
|
+
delegateQueue: nil
|
|
14
|
+
)
|
|
15
|
+
private var dataTask_: URLSessionDataTask?
|
|
16
|
+
private let responseBody = NSMutableData()
|
|
17
|
+
private var responseIsText = false
|
|
18
|
+
private var responseContentLength: Int64 = 0
|
|
19
|
+
|
|
20
|
+
// MARK: URLProtocol implementations
|
|
21
|
+
|
|
22
|
+
override class func canInit(with request: URLRequest) -> Bool {
|
|
23
|
+
guard let scheme = request.url?.scheme else {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
if !["http", "https"].contains(scheme) {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
let isNewRequest = URLProtocol.property(
|
|
30
|
+
forKey: EXDevLauncherRequestLoggerProtocol.REQUEST_ID,
|
|
31
|
+
in: request
|
|
32
|
+
) == nil
|
|
33
|
+
return isNewRequest
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override init(
|
|
37
|
+
request: URLRequest,
|
|
38
|
+
cachedResponse: CachedURLResponse?,
|
|
39
|
+
client: URLProtocolClient?
|
|
40
|
+
) {
|
|
41
|
+
super.init(request: request, cachedResponse: cachedResponse, client: client)
|
|
42
|
+
// swiftlint:disable force_cast
|
|
43
|
+
let mutableRequest = request as! MutableURLRequest
|
|
44
|
+
// swiftlint:enable force_cast
|
|
45
|
+
let redirectResponse = URLProtocol.property(
|
|
46
|
+
forKey: EXDevLauncherRequestLoggerProtocol.REDIRECT_RESPONSE,
|
|
47
|
+
in: request
|
|
48
|
+
) as? RedirectResponse
|
|
49
|
+
let requestId = redirectResponse?.requestId ?? EXDevLauncherRequestLoggerProtocol.requestIdProvider.create()
|
|
50
|
+
URLProtocol.setProperty(
|
|
51
|
+
requestId,
|
|
52
|
+
forKey: EXDevLauncherRequestLoggerProtocol.REQUEST_ID,
|
|
53
|
+
in: mutableRequest
|
|
54
|
+
)
|
|
55
|
+
EXDevLauncherNetworkLogger.shared.emitNetworkWillBeSent(
|
|
56
|
+
request: mutableRequest as URLRequest,
|
|
57
|
+
requestId: requestId,
|
|
58
|
+
redirectResponse: redirectResponse?.redirectResponse
|
|
59
|
+
)
|
|
60
|
+
dataTask_ = urlSession.dataTask(with: mutableRequest as URLRequest)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
|
|
64
|
+
request
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
override func startLoading() {
|
|
68
|
+
dataTask_?.resume()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override func stopLoading() {
|
|
72
|
+
dataTask_?.cancel()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// MARK: URLSessionDataDelegate implementations
|
|
76
|
+
|
|
77
|
+
func urlSession(_: URLSession, dataTask _: URLSessionDataTask, didReceive data: Data) {
|
|
78
|
+
client?.urlProtocol(self, didLoad: data)
|
|
79
|
+
if responseBody.length + data.count <= EXDevLauncherRequestLoggerProtocol.MAX_BODY_SIZE {
|
|
80
|
+
responseBody.append(data)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func urlSession(
|
|
85
|
+
_: URLSession,
|
|
86
|
+
dataTask: URLSessionDataTask,
|
|
87
|
+
didReceive response: URLResponse,
|
|
88
|
+
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
|
|
89
|
+
) {
|
|
90
|
+
if let resp = response as? HTTPURLResponse,
|
|
91
|
+
let currentRequest = dataTask.currentRequest,
|
|
92
|
+
let requestId = URLProtocol.property(
|
|
93
|
+
forKey: EXDevLauncherRequestLoggerProtocol.REQUEST_ID,
|
|
94
|
+
in: currentRequest
|
|
95
|
+
) as? String {
|
|
96
|
+
EXDevLauncherNetworkLogger.shared.emitNetworkResponse(
|
|
97
|
+
request: request,
|
|
98
|
+
requestId: requestId,
|
|
99
|
+
response: resp
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
let contentType = resp.value(forHTTPHeaderField: "Content-Type")
|
|
103
|
+
responseIsText = (contentType?.starts(with: "text/") ?? false) || contentType == "application/json"
|
|
104
|
+
responseContentLength = resp.expectedContentLength
|
|
105
|
+
}
|
|
106
|
+
completionHandler(.allow)
|
|
107
|
+
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
|
111
|
+
if let error = error {
|
|
112
|
+
client?.urlProtocol(self, didFailWithError: error)
|
|
113
|
+
} else {
|
|
114
|
+
client?.urlProtocolDidFinishLoading(self)
|
|
115
|
+
if responseContentLength > 0 && responseContentLength <= EXDevLauncherRequestLoggerProtocol.MAX_BODY_SIZE,
|
|
116
|
+
let currentRequest = task.currentRequest,
|
|
117
|
+
let requestId = URLProtocol.property(
|
|
118
|
+
forKey: EXDevLauncherRequestLoggerProtocol.REQUEST_ID,
|
|
119
|
+
in: currentRequest
|
|
120
|
+
) as? String {
|
|
121
|
+
EXDevLauncherNetworkLogger.shared.emitNetworkDidReceiveBody(
|
|
122
|
+
requestId: requestId, responseBody: responseBody as Data, isText: responseIsText)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func urlSession(
|
|
128
|
+
_: URLSession,
|
|
129
|
+
task _: URLSessionTask,
|
|
130
|
+
willPerformHTTPRedirection response: HTTPURLResponse,
|
|
131
|
+
newRequest request: URLRequest,
|
|
132
|
+
completionHandler: @escaping (URLRequest?) -> Void
|
|
133
|
+
) {
|
|
134
|
+
let redirectRequest: URLRequest
|
|
135
|
+
if let requestId = URLProtocol.property(forKey: EXDevLauncherRequestLoggerProtocol.REQUEST_ID, in: request) as? String {
|
|
136
|
+
// swiftlint:disable force_cast
|
|
137
|
+
let mutableRequest = request as! MutableURLRequest
|
|
138
|
+
// swiftlint:enable force_cast
|
|
139
|
+
URLProtocol.removeProperty(
|
|
140
|
+
forKey: EXDevLauncherRequestLoggerProtocol.REQUEST_ID,
|
|
141
|
+
in: mutableRequest
|
|
142
|
+
)
|
|
143
|
+
URLProtocol.setProperty(
|
|
144
|
+
RedirectResponse(requestId: requestId, redirectResponse: response),
|
|
145
|
+
forKey: EXDevLauncherRequestLoggerProtocol.REDIRECT_RESPONSE,
|
|
146
|
+
in: mutableRequest
|
|
147
|
+
)
|
|
148
|
+
redirectRequest = mutableRequest as URLRequest
|
|
149
|
+
} else {
|
|
150
|
+
redirectRequest = request
|
|
151
|
+
}
|
|
152
|
+
completionHandler(redirectRequest)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
Data structure to save the response for redirection
|
|
157
|
+
*/
|
|
158
|
+
private struct RedirectResponse {
|
|
159
|
+
let requestId: String
|
|
160
|
+
let redirectResponse: HTTPURLResponse
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
A helper class to create a unique request ID
|
|
165
|
+
*/
|
|
166
|
+
private struct RequestIdProvider {
|
|
167
|
+
private var value: UInt64 = 0
|
|
168
|
+
|
|
169
|
+
mutating func create() -> String {
|
|
170
|
+
// We could ensure the increment thread safety,
|
|
171
|
+
// because we access this function from the same thread (com.apple.CFNetwork.CustomProtocols).
|
|
172
|
+
value += 1
|
|
173
|
+
return String(value)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
`URLRequest.httpBodyData()` extension to read the underlying `httpBodyStream` as Data.
|
|
180
|
+
Only read at maximum `EXDevLauncherRequestLoggerProtocol.MAX_BODY_SIZE` bytes.
|
|
181
|
+
*/
|
|
182
|
+
extension URLRequest {
|
|
183
|
+
func httpBodyData() -> Data? {
|
|
184
|
+
if let httpBody = self.httpBody {
|
|
185
|
+
return httpBody
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if let contentLength = self.allHTTPHeaderFields?["Content-Length"],
|
|
189
|
+
let contentLengthInt = Int(contentLength),
|
|
190
|
+
contentLengthInt > EXDevLauncherRequestLoggerProtocol.MAX_BODY_SIZE {
|
|
191
|
+
return nil
|
|
192
|
+
}
|
|
193
|
+
guard let stream = self.httpBodyStream else {
|
|
194
|
+
return nil
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let bufferSize: Int = 8192
|
|
198
|
+
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
|
|
199
|
+
|
|
200
|
+
stream.open()
|
|
201
|
+
defer {
|
|
202
|
+
buffer.deallocate()
|
|
203
|
+
stream.close()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
var data = Data()
|
|
207
|
+
while stream.hasBytesAvailable {
|
|
208
|
+
let chunkSize = stream.read(buffer, maxLength: bufferSize)
|
|
209
|
+
if data.count + chunkSize > EXDevLauncherRequestLoggerProtocol.MAX_BODY_SIZE {
|
|
210
|
+
return nil
|
|
211
|
+
}
|
|
212
|
+
data.append(buffer, count: chunkSize)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return data
|
|
216
|
+
}
|
|
217
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-dev-launcher",
|
|
3
3
|
"title": "Expo Development Launcher",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.2.0",
|
|
5
5
|
"description": "Pre-release version of the Expo development launcher package for testing.",
|
|
6
6
|
"main": "build/DevLauncher.js",
|
|
7
7
|
"types": "build/DevLauncher.d.ts",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"license": "MIT",
|
|
30
30
|
"homepage": "https://docs.expo.dev",
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"expo-dev-menu": "2.
|
|
32
|
+
"expo-dev-menu": "2.2.0",
|
|
33
33
|
"resolve-from": "^5.0.0",
|
|
34
34
|
"semver": "^7.3.5"
|
|
35
35
|
},
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"graphql": "^16.0.1",
|
|
50
50
|
"graphql-request": "^3.6.1",
|
|
51
51
|
"react": "18.2.0",
|
|
52
|
-
"react-native": "0.71.
|
|
52
|
+
"react-native": "0.71.6",
|
|
53
53
|
"react-query": "^3.34.16",
|
|
54
54
|
"url": "^0.11.0"
|
|
55
55
|
},
|
|
@@ -63,5 +63,5 @@
|
|
|
63
63
|
"./setupTests.ts"
|
|
64
64
|
]
|
|
65
65
|
},
|
|
66
|
-
"gitHead": "
|
|
66
|
+
"gitHead": "362ed24e78a57e9839afd2925d2af4aff7e28437"
|
|
67
67
|
}
|