first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

81
node_modules/@expo/log-box/android/build.gradle generated vendored Normal file
View File

@@ -0,0 +1,81 @@
import org.apache.tools.ant.taskdefs.condition.Os
plugins {
id 'com.android.library'
id 'expo-module-gradle-plugin'
}
group = 'host.exp.exponent'
version = '55.0.7'
expoModule {
canBePublished false
}
def isEnabled = System.getenv('EXPO_UNSTABLE_LOG_BOX') == '1' || System.getenv('EXPO_UNSTABLE_LOG_BOX')?.toLowerCase() == 'true'
android {
namespace "expo.modules.logbox"
defaultConfig {
versionCode 1
versionName "55.0.7"
buildConfigField "boolean", "EXPO_UNSTABLE_LOG_BOX", isEnabled ? "true" : "false"
}
sourceSets {
main {
java.srcDirs += "src/main"
}
if (isEnabled) {
debug {
// dist contains only ExpoLogBox.bundle
assets.srcDirs += "${projectDir}/../dist"
}
}
}
aaptOptions {
ignoreAssetsPattern '!._expo'
}
}
dependencies {
implementation 'com.facebook.react:react-android'
implementation 'com.google.android.material:material:1.13.0'
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
implementation 'com.google.code.gson:gson:2.8.6'
}
def expoLogBoxDirFile = project.projectDir.parentFile
def expoLogBoxDir = expoLogBoxDirFile.absolutePath
def config = project.hasProperty("react") ? project.react : [];
def nodeExecutableAndArgs = config.nodeExecutableAndArgs ?: ["node"]
afterEvaluate {
def distDirFile = file("${expoLogBoxDir}/dist")
def bundleOnDemandFlagFile = file("${expoLogBoxDir}/.bundle-on-demand")
if (bundleOnDemandFlagFile.exists()) {
def createExpoLogBoxBundleTask = tasks.register('createExpoLogBoxBundle', Exec) {
description = 'expo-log-box: Create bundle.'
// Setup inputs and output so Gradle can cache the ExpoLogBox.bundle
inputs.dir project.fileTree(expoLogBoxDirFile) {
exclude 'dist/**'
exclude 'android/**'
}
outputs.dir distDirFile
// Generate the bundle
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
commandLine("cmd", "/c", *nodeExecutableAndArgs, "$expoLogBoxDir/scripts/build-bundle.mjs")
} else {
commandLine(*nodeExecutableAndArgs, "$expoLogBoxDir/scripts/build-bundle.mjs")
}
}
// Generate bundle at preBuild
tasks.getByName('preBuild').dependsOn(createExpoLogBoxBundleTask)
}
}

View File

@@ -0,0 +1,3 @@
<manifest>
</manifest>

View File

@@ -0,0 +1,137 @@
/*
* Copyright © 2024 650 Industries.
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* https://github.com/krystofwoldrich/react-native/blob/7db31e2fca0f828aa6bf489ae6dc4adef9b7b7c3/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.kt
*/
package expo.modules.logbox
import android.content.Context
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.common.SurfaceDelegate
import com.facebook.react.common.SurfaceDelegateFactory
import com.facebook.react.devsupport.DevSupportManagerBase
import com.facebook.react.devsupport.ReactInstanceDevHelper
import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener
import com.facebook.react.devsupport.interfaces.DevLoadingViewManager
import com.facebook.react.devsupport.interfaces.DevSupportManager
import com.facebook.react.devsupport.interfaces.PausedInDebuggerOverlayManager
import com.facebook.react.devsupport.interfaces.RedBoxHandler
import com.facebook.react.packagerconnection.RequestHandler
import com.facebook.react.devsupport.StackTraceHelper.convertJavaStackTrace
import com.facebook.react.devsupport.StackTraceHelper.convertJsStackTrace
import com.facebook.react.devsupport.interfaces.StackFrame
class ExpoLogBoxDevSupportManager(
applicationContext: Context,
reactInstanceManagerHelper: ReactInstanceDevHelper,
packagerPathForJSBundleName: String?,
enableOnCreate: Boolean,
redBoxHandler: RedBoxHandler?,
devBundleDownloadListener: DevBundleDownloadListener?,
minNumShakes: Int,
customPackagerCommandHandlers: Map<String, RequestHandler>?,
surfaceDelegateFactory: SurfaceDelegateFactory?,
devLoadingViewManager: DevLoadingViewManager?,
pausedInDebuggerOverlayManager: PausedInDebuggerOverlayManager?
) :
ExpoBridgelessDevSupportManager(
applicationContext,
reactInstanceManagerHelper,
packagerPathForJSBundleName,
enableOnCreate,
redBoxHandler,
devBundleDownloadListener,
minNumShakes,
customPackagerCommandHandlers,
surfaceDelegateFactory,
devLoadingViewManager,
pausedInDebuggerOverlayManager
) {
private var redBoxSurfaceDelegate: SurfaceDelegate? = null
override fun hideRedboxDialog() {
redBoxSurfaceDelegate?.hide()
}
override fun showNewJavaError(message: String?, e: Throwable) {
showNewError(message, convertJavaStackTrace(e))
}
override fun showNewJSError(message: String?, details: ReadableArray?, errorCookie: Int) {
showNewError(message, convertJsStackTrace(details))
}
private fun showNewError(message: String?, stack: Array<StackFrame>) {
UiThreadUtil.runOnUiThread {
lastErrorTitle = message
lastErrorStack = stack
if (redBoxSurfaceDelegate == null) {
this.redBoxSurfaceDelegate =
createSurfaceDelegate("RedBox")
?: ExpoLogBoxSurfaceDelegate(this@ExpoLogBoxDevSupportManager).apply {
createContentView("RedBox")
}
}
if (redBoxSurfaceDelegate?.isShowing() == true) {
// Sometimes errors cause multiple errors to be thrown in JS in quick succession. Only
// show the first and most actionable one.
return@runOnUiThread
}
redBoxSurfaceDelegate?.show()
}
}
}
/**
* [Source](https://github.com/krystofwoldrich/react-native/blob/7db31e2fca0f828aa6bf489ae6dc4adef9b7b7c3/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/BridgelessDevSupportManager.kt#L29)
*
* An implementation of [DevSupportManager] that extends the functionality in
* [DevSupportManagerBase] with some additional, more flexible APIs for asynchronously loading the
* JS bundle.
*/
open class ExpoBridgelessDevSupportManager(
applicationContext: Context,
reactInstanceManagerHelper: ReactInstanceDevHelper,
packagerPathForJSBundleName: String?,
enableOnCreate: Boolean,
redBoxHandler: RedBoxHandler?,
devBundleDownloadListener: DevBundleDownloadListener?,
minNumShakes: Int,
customPackagerCommandHandlers: Map<String, RequestHandler>?,
surfaceDelegateFactory: SurfaceDelegateFactory?,
devLoadingViewManager: DevLoadingViewManager?,
pausedInDebuggerOverlayManager: PausedInDebuggerOverlayManager?
) :
DevSupportManagerBase(
applicationContext,
reactInstanceManagerHelper,
packagerPathForJSBundleName,
enableOnCreate,
redBoxHandler,
devBundleDownloadListener,
minNumShakes,
customPackagerCommandHandlers,
surfaceDelegateFactory,
devLoadingViewManager,
pausedInDebuggerOverlayManager
) {
override val uniqueTag: String
get() = "Bridgeless"
override fun handleReloadJS() {
UiThreadUtil.assertOnUiThread()
// dismiss RedBox if it exists
hideRedboxDialog()
reactInstanceDevHelper.reload("BridgelessDevSupportManager.handleReloadJS()")
}
}

View File

@@ -0,0 +1,24 @@
package expo.modules.logbox
import android.app.Activity
import android.content.Context
import android.os.Bundle
import com.facebook.react.ReactApplication
import expo.modules.core.interfaces.Package
import expo.modules.core.interfaces.ReactActivityLifecycleListener
class ExpoLogBoxPackage : Package {
override fun createReactActivityLifecycleListeners(activityContext: Context?): List<ReactActivityLifecycleListener> {
if (!BuildConfig.DEBUG || !BuildConfig.EXPO_UNSTABLE_LOG_BOX) {
return emptyList()
}
return listOf(
object : ReactActivityLifecycleListener {
override fun onCreate(activity: Activity, savedInstanceState: Bundle?) {
injectExpoLogBoxDevSupportManager((activity.application as ReactApplication).reactHost)
}
}
)
}
}

View File

@@ -0,0 +1,31 @@
package expo.modules.logbox
import java.lang.reflect.Field
import java.lang.reflect.Modifier
fun <T> Class<out T>.setProtectedDeclaredField(obj: T, filedName: String, newValue: Any, predicate: (Any?) -> Boolean = { true }) {
val field = getDeclaredField(filedName)
val modifiersField = Field::class.java.getDeclaredField("accessFlags")
field.isAccessible = true
modifiersField.isAccessible = true
modifiersField.setInt(
field,
field.modifiers and Modifier.FINAL.inv()
)
if (!predicate.invoke(field.get(obj))) {
return
}
field.set(obj, newValue)
}
fun <T, U> Class<out T>.getProtectedFieldValue(obj: T, filedName: String): U {
val field = getDeclaredField(filedName)
field.isAccessible = true
@Suppress("UNCHECKED_CAST")
return field.get(obj) as U
}

View File

@@ -0,0 +1,60 @@
package expo.modules.logbox
import android.util.Log
import com.facebook.react.ReactHost
import com.facebook.react.devsupport.DevSupportManagerBase
import com.facebook.react.devsupport.interfaces.DevSupportManager
import com.facebook.react.runtime.ReactHostImpl
fun injectExpoLogBoxDevSupportManager(reactHost: ReactHost?) {
val currentDevSupportManager = reactHost?.devSupportManager
if (currentDevSupportManager == null) {
Log.w(
"ExpoLogBox",
"ReactHost initialized without a dev support manager, ExpoLogBox can't be initialized."
)
return
} else if (currentDevSupportManager is ExpoLogBoxDevSupportManager) {
Log.i(
"ExpoLogBox",
"DevSupportManager is already `ExpoDevSupportManagerWithLogBoxOverride`, skipping initialization."
)
return
}
// NOTE(@krystofwoldrich): This will overwrite expo-dev-client dev support manager
try {
val newDevSupportManager =
createExpoLogBoxBridgelessDevSupportManager(
currentDevSupportManager
)
ReactHostImpl::class.java.setProtectedDeclaredField(
reactHost,
"devSupportManager",
newDevSupportManager
)
} catch (e: Exception) {
Log.i("ExpoLogBox", "Couldn't inject `ExpoDevSupportManagerWithLogBoxOverride`.", e)
}
}
fun createExpoLogBoxBridgelessDevSupportManager(
currentDevSupportManager: DevSupportManager,
devManagerClass: Class<*> = DevSupportManagerBase::class.java
): ExpoLogBoxDevSupportManager {
return ExpoLogBoxDevSupportManager(
applicationContext = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "applicationContext"),
reactInstanceManagerHelper = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "reactInstanceDevHelper"),
packagerPathForJSBundleName = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "jsAppBundleName"),
enableOnCreate = true,
redBoxHandler = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "redBoxHandler"),
devBundleDownloadListener = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "devBundleDownloadListener"),
minNumShakes = 1,
customPackagerCommandHandlers = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "customPackagerCommandHandlers"),
surfaceDelegateFactory = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "surfaceDelegateFactory"),
devLoadingViewManager = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "devLoadingViewManager"),
pausedInDebuggerOverlayManager = devManagerClass.getProtectedFieldValue(currentDevSupportManager, "pausedInDebuggerOverlayManager")
)
}

View File

@@ -0,0 +1,149 @@
package expo.modules.logbox
import android.app.Activity
import android.app.Dialog
import android.widget.FrameLayout
import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.ReactContext
import com.facebook.react.common.SurfaceDelegate
import com.facebook.react.devsupport.interfaces.DevSupportManager
import okhttp3.Call
import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import java.io.IOException
class ExpoLogBoxSurfaceDelegate(private val devSupportManager: DevSupportManager) :
SurfaceDelegate {
private var dialog: Dialog? = null
override fun createContentView(appKey: String) = Unit
override fun isContentViewReady(): Boolean {
return true
}
override fun destroyContentView() = Unit
override fun show() {
val context: Activity = devSupportManager.currentActivity ?: return
if (context.isFinishing) {
devSupportManager.currentReactContext?.let { reactContext ->
/**
* If the activity isn't available, try again after the next onHostResume(). onHostResume()
* is when the activity gets attached to the react native.
*/
runAfterHostResume(reactContext) { this.show() }
return
}
return
}
dialog = Dialog(context, android.R.style.Theme_NoTitleBar)
val rootContainer = FrameLayout(context).apply {
fitsSystemWindows = true
}
val errorMessage = devSupportManager.lastErrorTitle
val errorStack = devSupportManager.lastErrorStack?.map { frame ->
mapOf(
// Expected to match https://github.com/expo/expo/blob/5ed042a3547571fa70cf1d53db7c12b4bb966a90/packages/%40expo/log-box/src/devServerEndpoints.ts#L3
"file" to frame.file,
"methodName" to frame.method,
"arguments" to emptyArray<String>(),
"lineNumber" to frame.line,
"column" to frame.column,
"collapse" to frame.isCollapsed
)
}
val webViewWrapper = ExpoLogBoxWebViewWrapper(
actions = ExpoLogBoxWebViewWrapper.Actions(
onReload = ExpoLogBoxWebViewWrapper.Actions.OnReload(onReload),
fetchTextAsync = ExpoLogBoxWebViewWrapper.Actions.FetchTextAsync(fetchTextAsync)
),
props = mapOf(
"platform" to "android",
"nativeLogs" to arrayOf(
mapOf(
"message" to errorMessage,
"stack" to errorStack
)
)
),
context
)
rootContainer.addView(webViewWrapper.webView)
dialog?.setContentView(rootContainer)
dialog?.show()
}
override fun hide() {
// Build Errors are generally not dismissable
// NOTE: The hide function is also called also when application goes to background
// which causes the error disappear and leave the app on black screen
dialog?.dismiss()
}
override fun isShowing(): Boolean {
return dialog?.isShowing == true
}
private val onReload = {
devSupportManager.handleReloadJS()
}
private val fetchTextAsync = {
url: String,
method: String,
body: String,
onResult: (String) -> Unit,
onFailure: (Exception) -> Unit
->
val client = OkHttpClient()
val requestBody = if (method.uppercase() != "GET") {
body.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
} else {
null
}
val request = Request.Builder()
.url(url)
.method(method.uppercase(), requestBody)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
onFailure(e)
}
override fun onResponse(call: Call, response: Response) {
response.use {
val responseBody = it.body?.string() ?: "{}"
onResult(responseBody)
}
}
})
}
companion object {
private fun runAfterHostResume(reactContext: ReactContext, runnable: Runnable) {
reactContext.addLifecycleEventListener(
object : LifecycleEventListener {
override fun onHostResume() {
runnable.run()
reactContext.removeLifecycleEventListener(this)
}
override fun onHostPause() = Unit
override fun onHostDestroy() = Unit
}
)
}
}
}

View File

@@ -0,0 +1,224 @@
// Keep in sync with webview-wrapper.tsx
// https://github.com/expo/expo/blob/main/packages/expo/src/dom/webview-wrapper.tsx
package expo.modules.logbox
import android.app.Activity
import android.graphics.Bitmap
import android.graphics.Color
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebView.setWebContentsDebuggingEnabled
import android.webkit.WebViewClient
import com.facebook.react.modules.systeminfo.AndroidInfoHelpers
import com.google.gson.Gson
import com.google.gson.JsonObject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ExpoLogBoxWebViewWrapper(
val actions: Actions,
val props: Map<String, Any>,
val context: Activity
) {
val webView: WebView = WebView(context).apply {
setBackgroundColor(Color.BLACK)
settings.javaScriptEnabled = true
setWebContentsDebuggingEnabled(true)
// This interface is defined by the Expo DOM Components WebView Wrapper
// and must always be the same as [add link]
addJavascriptInterface(
object : Any() {
@JavascriptInterface
fun postMessage(rawMessage: String) {
processMessageFromWebView(rawMessage)
}
},
"ReactNativeWebView"
)
webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
initializeLogBoxDomEnvironment()
}
}
// TODO: use build config to specify the dev url
// webView.loadUrl("http://10.0.2.2:8090/")
loadUrl("file:///android_asset/ExpoLogBox.bundle/index.html")
}
private fun initializeLogBoxDomEnvironment() {
val initialProps = mapOf(
"names" to actions.getNames(),
"props" to props
)
val gson = Gson()
val jsonObject = gson.toJson(initialProps)
val devServerOrigin = "http://${AndroidInfoHelpers.getServerHost(context)}"
val script = """
var process=globalThis.process||{};process.env=process.env||{};
process.env.EXPO_DEV_SERVER_ORIGIN='$devServerOrigin';
window.$$${"EXPO_DOM_HOST_OS"} = 'android';
window.$$${"EXPO_INITIAL_PROPS"} = $jsonObject;
""".trimIndent()
webView.post {
webView.evaluateJavascript(script, null)
}
}
private fun processMessageFromWebView(rawMessage: String) {
val gson = Gson()
val jsonObject = gson.fromJson(rawMessage, JsonObject::class.java)
val messageType = jsonObject.getAsJsonPrimitive("type")
if (messageType.isString && messageType.asString == NATIVE_ACTION) {
val data = jsonObject.getAsJsonObject("data")
val actionId = data.getAsJsonPrimitive("actionId")
val uid = data.getAsJsonPrimitive("uid")
val args = data.getAsJsonArray("args")
if (!actionId.isString || !uid.isString || !args.isJsonArray) {
return
}
when (actionId.asString) {
"onReload" -> {
actions.onReload.action()
}
"fetchTextAsync" -> {
CoroutineScope(Dispatchers.Default).launch {
val url = when {
args.get(0).isJsonPrimitive &&
args.get(0).asJsonPrimitive.isString
-> args.get(0).asJsonPrimitive.asString
else -> null
}
val options = args.get(1).asJsonObject
val method = when {
options.has("method") &&
options.get("method").isJsonPrimitive &&
options.getAsJsonPrimitive("method").isString
-> options.getAsJsonPrimitive("method").asString
else -> null
}
val body = when {
options.has("body") &&
options.get("body").isJsonPrimitive &&
options.getAsJsonPrimitive("body").isString
-> options.getAsJsonPrimitive("body").asString
else -> null
}
if (url != null) {
actions.fetchTextAsync.action(
url,
method ?: "GET",
body ?: "",
{ result ->
sendReturn(result, uid.asString, actionId.asString)
},
{ exception ->
sendReturn(exception, uid.asString, actionId.asString)
}
)
}
}
}
}
}
}
fun sendReturn(result: Any, uid: String, actionId: String) {
sendReturn(
mapOf(
"type" to NATIVE_ACTION_RESULT,
"data" to mapOf(
"uid" to uid,
"actionId" to actionId,
"result" to result
)
)
)
}
fun sendReturn(exception: Exception, uid: String, actionId: String) {
sendReturn(
mapOf(
"type" to NATIVE_ACTION_RESULT,
"data" to mapOf(
"uid" to uid,
"actionId" to actionId,
"error" to mapOf(
"message" to "$exception"
)
)
)
)
}
fun sendReturn(data: Map<String, Any>) {
sendReturn(
Gson().toJson(
mapOf(
"detail" to data
)
)
)
}
fun sendReturn(value: String) {
val injectedJavascript = """
;
(function() {
try {
console.log("received", $value)
window.dispatchEvent(new CustomEvent("${DOM_EVENT}", $value));
} catch (e) {
console.log('error', e)
}
})();
true;
"""
webView.post {
webView.evaluateJavascript(injectedJavascript, null)
}
}
companion object {
private const val DOM_EVENT = "$\$dom_event"
private const val NATIVE_ACTION_RESULT = "$\$native_action_result"
private const val NATIVE_ACTION = "$\$native_action"
}
data class Actions(
val onReload: OnReload,
val fetchTextAsync: FetchTextAsync
) {
fun getNames(): Array<String> {
return arrayOf(
onReload.name,
fetchTextAsync.name
)
}
data class OnReload(
val action: () -> Unit,
val name: String = "onReload"
)
data class FetchTextAsync(
val action: (
url: String,
method: String,
body: String,
onResult: (String) -> Unit,
onFailure: (Exception) -> Unit
) -> Unit,
val name: String = "fetchTextAsync"
)
}
}