From eaa0aca175cf964437572933309793a502d84535 Mon Sep 17 00:00:00 2001 From: Ray <153027766+ramonclaudio@users.noreply.github.com> Date: Sat, 23 May 2026 03:46:15 -0400 Subject: [PATCH 1/4] [ci] Fork-safety audit (#46050) --- .github/workflows/bare-diffs.yml | 2 +- .github/workflows/cli.yml | 4 ++++ .github/workflows/create-expo-app.yml | 1 + .github/workflows/create-expo-module.yml | 1 + .github/workflows/development-client-latest-e2e.yml | 2 +- .github/workflows/fingerprint.yml | 1 + .github/workflows/ios-prebuild-external-xcframeworks.yml | 6 +++--- .github/workflows/ios-static-frameworks.yml | 3 ++- .github/workflows/issue-stale.yml | 1 + .github/workflows/lock.yml | 1 + .github/workflows/native-component-list.yml | 2 +- .github/workflows/sdk.yml | 7 ++++--- .github/workflows/test-react-native-nightly.yml | 2 ++ .github/workflows/test-suite-macos.yml | 2 +- .github/workflows/test-suite-nightly.yml | 4 ++++ .github/workflows/test-suite.yml | 8 ++++---- 16 files changed, 32 insertions(+), 15 deletions(-) diff --git a/.github/workflows/bare-diffs.yml b/.github/workflows/bare-diffs.yml index 3ad00f407a9372..98805a14df1f15 100644 --- a/.github/workflows/bare-diffs.yml +++ b/.github/workflows/bare-diffs.yml @@ -37,7 +37,7 @@ jobs: pnpm expotools generate-bare-diffs --check - name: 🔔 Notify on Slack uses: ./.github/actions/slack-notify - if: failure() && github.event.ref == 'refs/heads/main' + if: failure() && github.event.ref == 'refs/heads/main' && github.repository == 'expo/expo' with: webhook: ${{ secrets.slack_webhook_docs }} author_name: Check for Changes in Bare Diffs diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 24e5af5fde1ca2..0d47a9390cda8e 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -55,6 +55,7 @@ concurrency: jobs: jest-ubuntu: + if: github.event_name != 'schedule' || github.repository == 'expo/expo' runs-on: ubuntu-24.04 strategy: fail-fast: false @@ -98,6 +99,7 @@ jobs: run: pnpm test:e2e --max-workers 1 --shard ${{ matrix.shard }}/${{ strategy.job-total }} jest-windows: + if: github.event_name != 'schedule' || github.repository == 'expo/expo' runs-on: windows-2022 strategy: fail-fast: false @@ -157,6 +159,7 @@ jobs: run: pnpm test:e2e --max-workers 1 --shard ${{ matrix.shard }}/${{ strategy.job-total }} playwright-ubuntu: + if: github.event_name != 'schedule' || github.repository == 'expo/expo' runs-on: ubuntu-24.04 steps: - name: 👀 Checkout @@ -216,6 +219,7 @@ jobs: retention-days: 30 playwright-windows: + if: github.event_name != 'schedule' || github.repository == 'expo/expo' runs-on: windows-2022 strategy: fail-fast: false diff --git a/.github/workflows/create-expo-app.yml b/.github/workflows/create-expo-app.yml index c1c3041d1cd2b4..04fbbf32f30d7a 100644 --- a/.github/workflows/create-expo-app.yml +++ b/.github/workflows/create-expo-app.yml @@ -21,6 +21,7 @@ concurrency: jobs: test: + if: github.event_name != 'schedule' || github.repository == 'expo/expo' runs-on: ubuntu-24.04 strategy: fail-fast: false diff --git a/.github/workflows/create-expo-module.yml b/.github/workflows/create-expo-module.yml index 295bc06aa68919..dc13247d6c2fd5 100644 --- a/.github/workflows/create-expo-module.yml +++ b/.github/workflows/create-expo-module.yml @@ -21,6 +21,7 @@ concurrency: jobs: test: + if: github.event_name != 'schedule' || github.repository == 'expo/expo' runs-on: ubuntu-24.04 strategy: fail-fast: false diff --git a/.github/workflows/development-client-latest-e2e.yml b/.github/workflows/development-client-latest-e2e.yml index 371b331d7fa420..04d82c0f6bdedd 100644 --- a/.github/workflows/development-client-latest-e2e.yml +++ b/.github/workflows/development-client-latest-e2e.yml @@ -75,7 +75,7 @@ jobs: path: packages/expo-dev-client/artifacts - name: 🔔 Notify on Slack uses: ./.github/actions/slack-notify - if: failure() && github.repository == 'expo/expo' + if: failure() with: webhook: ${{ secrets.slack_webhook_dev_client }} channel: '#dev-clients' diff --git a/.github/workflows/fingerprint.yml b/.github/workflows/fingerprint.yml index 2971640b3e700f..d1b85d7037f9e0 100644 --- a/.github/workflows/fingerprint.yml +++ b/.github/workflows/fingerprint.yml @@ -24,6 +24,7 @@ concurrency: jobs: test: + if: github.event_name != 'schedule' || github.repository == 'expo/expo' strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] diff --git a/.github/workflows/ios-prebuild-external-xcframeworks.yml b/.github/workflows/ios-prebuild-external-xcframeworks.yml index 7da37ecf4cecf2..fe2a19b5a39de7 100644 --- a/.github/workflows/ios-prebuild-external-xcframeworks.yml +++ b/.github/workflows/ios-prebuild-external-xcframeworks.yml @@ -87,20 +87,20 @@ jobs: include-hidden-files: true - name: Authenticate to Google Cloud - if: ${{ success() && github.event.inputs.publish == 'true' }} + if: ${{ success() && github.event.inputs.publish == 'true' && github.repository == 'expo/expo' }} uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3 with: project_id: exponentjs workload_identity_provider: projects/321830142373/locations/global/workloadIdentityPools/github/providers/expo - name: Setup gcloud - if: ${{ success() && github.event.inputs.publish == 'true' }} + if: ${{ success() && github.event.inputs.publish == 'true' && github.repository == 'expo/expo' }} uses: google-github-actions/setup-gcloud@e427ad8a34f8676edf47cf7d7925499adf3eb74f # v2 with: project_id: exponentjs - name: Upload XCFramework directory structure to GCS - if: ${{ success() && github.event.inputs.publish == 'true' }} + if: ${{ success() && github.event.inputs.publish == 'true' && github.repository == 'expo/expo' }} run: | SYNC_ROOT="$RUNNER_TEMP/precompiled-modules-upload" rm -rf "$SYNC_ROOT" diff --git a/.github/workflows/ios-static-frameworks.yml b/.github/workflows/ios-static-frameworks.yml index ac8a8c5a840866..a9ede48bdb156e 100644 --- a/.github/workflows/ios-static-frameworks.yml +++ b/.github/workflows/ios-static-frameworks.yml @@ -14,6 +14,7 @@ concurrency: jobs: build: + if: github.event_name != 'schedule' || github.repository == 'expo/expo' runs-on: macos-15 steps: - name: 👀 Checkout @@ -53,7 +54,7 @@ jobs: xcodebuild -workspace ios/BareExpo.xcworkspace -scheme BareExpo -configuration Release -sdk iphonesimulator -derivedDataPath "ios/build" | xcpretty - name: 🔔 Notify on Slack uses: ./.github/actions/slack-notify - if: failure() && (github.event_name == 'schedule' || github.event.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/heads/sdk-')) + if: failure() && (github.event_name == 'schedule' || github.event.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/heads/sdk-')) && github.repository == 'expo/expo' with: webhook: ${{ secrets.slack_webhook_ios }} channel: '#expo-ios' diff --git a/.github/workflows/issue-stale.yml b/.github/workflows/issue-stale.yml index 6b5b091f44a538..3e860b5bfecd5a 100644 --- a/.github/workflows/issue-stale.yml +++ b/.github/workflows/issue-stale.yml @@ -6,6 +6,7 @@ on: jobs: close-issues: + if: github.repository == 'expo/expo' runs-on: ubuntu-24.04 steps: - uses: actions/stale@a20b814fb01b71def3bd6f56e7494d667ddf28da # v4 diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index a432b5c942613b..5eb7814d48eeda 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -15,6 +15,7 @@ concurrency: jobs: action: + if: github.repository == 'expo/expo' runs-on: ubuntu-24.04 steps: - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5 diff --git a/.github/workflows/native-component-list.yml b/.github/workflows/native-component-list.yml index dcd806afbaec6a..dc151b0872b70c 100644 --- a/.github/workflows/native-component-list.yml +++ b/.github/workflows/native-component-list.yml @@ -49,7 +49,7 @@ jobs: working-directory: apps/native-component-list - name: 🔔 Notify on Slack uses: ./.github/actions/slack-notify - if: failure() && (github.event.ref == 'refs/heads/main') + if: failure() && (github.event.ref == 'refs/heads/main') && github.repository == 'expo/expo' with: webhook: ${{ secrets.slack_webhook_api }} author_name: Build Native Component List diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml index 84525eba02db1c..35c631986c6d62 100644 --- a/.github/workflows/sdk.yml +++ b/.github/workflows/sdk.yml @@ -31,6 +31,7 @@ concurrency: jobs: check-packages: + if: github.event_name != 'schedule' || github.repository == 'expo/expo' runs-on: ubuntu-24.04 steps: - name: 👀 Checkout @@ -75,14 +76,14 @@ jobs: run: pnpm expotools check-packages --since $SINCE --core - name: 🔔 Notify on Slack uses: ./.github/actions/slack-notify - if: failure() && (github.event_name == 'schedule' || github.event.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/heads/sdk-')) + if: failure() && (github.event_name == 'schedule' || github.event.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/heads/sdk-')) && github.repository == 'expo/expo' with: webhook: ${{ secrets.slack_webhook_api }} author_name: Check packages check-packages-windows: runs-on: windows-latest - if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} + if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'schedule' && github.repository == 'expo/expo') }} steps: - name: 👀 Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 @@ -106,7 +107,7 @@ jobs: run: pnpm expotools check-packages --all - name: 🔔 Notify on Slack uses: ./.github/actions/slack-notify - if: failure() && (github.event_name == 'schedule' || github.event.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/heads/sdk-')) + if: failure() && (github.event_name == 'schedule' || github.event.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/heads/sdk-')) && github.repository == 'expo/expo' with: webhook: ${{ secrets.slack_webhook_api }} author_name: Check packages diff --git a/.github/workflows/test-react-native-nightly.yml b/.github/workflows/test-react-native-nightly.yml index 8e9dc529b704b9..93cc1f6ccb863d 100644 --- a/.github/workflows/test-react-native-nightly.yml +++ b/.github/workflows/test-react-native-nightly.yml @@ -20,6 +20,7 @@ concurrency: jobs: ios-build: + if: github.repository == 'expo/expo' strategy: fail-fast: true matrix: @@ -87,6 +88,7 @@ jobs: author_name: React Native Nightly (iOS) android-build: + if: github.repository == 'expo/expo' strategy: fail-fast: true matrix: diff --git a/.github/workflows/test-suite-macos.yml b/.github/workflows/test-suite-macos.yml index 87fc54dcdfb6ad..0b1eb6a4dd4d95 100644 --- a/.github/workflows/test-suite-macos.yml +++ b/.github/workflows/test-suite-macos.yml @@ -113,7 +113,7 @@ jobs: path: apps/bare-expo/macos/build/BareExpo.app - name: 🔔 Notify on Slack uses: ./.github/actions/slack-notify - if: failure() && (github.event.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/heads/sdk-')) + if: failure() && (github.event.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/heads/sdk-')) && github.repository == 'expo/expo' with: webhook: ${{ secrets.slack_webhook_ios }} channel: '#expo-ios' diff --git a/.github/workflows/test-suite-nightly.yml b/.github/workflows/test-suite-nightly.yml index 31a3170aed2c16..888240085463d8 100644 --- a/.github/workflows/test-suite-nightly.yml +++ b/.github/workflows/test-suite-nightly.yml @@ -20,6 +20,7 @@ concurrency: jobs: ios-build: + if: github.repository == 'expo/expo' runs-on: macos-15 steps: - name: 👀 Checkout @@ -87,6 +88,7 @@ jobs: author_name: Test Suite Nightly (iOS) ios-test: + if: github.repository == 'expo/expo' needs: ios-build runs-on: macos-15 steps: @@ -153,6 +155,7 @@ jobs: author_name: Test Suite Nightly (iOS) android-build: + if: github.repository == 'expo/expo' runs-on: ubuntu-24.04 env: ORG_GRADLE_PROJECT_reactNativeArchitectures: x86_64 @@ -216,6 +219,7 @@ jobs: author_name: Test Suite Nightly (Android) android-test: + if: github.repository == 'expo/expo' needs: android-build runs-on: ubuntu-24.04 strategy: diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 0144e6919abf05..02d23cc3c699d1 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -230,7 +230,7 @@ jobs: run: ccache -s -v - name: 🔔 Notify on Slack uses: ./.github/actions/slack-notify - if: failure() && (github.event.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/heads/sdk-')) + if: failure() && (github.event.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/heads/sdk-')) && github.repository == 'expo/expo' with: webhook: ${{ secrets.slack_webhook_ios }} channel: '#expo-ios' @@ -325,7 +325,7 @@ jobs: echo "Artifacts URL: ${{ steps.upload-artifacts.outputs.artifact-url }}" - name: 🔔 Notify on Slack uses: ./.github/actions/slack-notify - if: failure() && (github.event.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/heads/sdk-')) + if: failure() && (github.event.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/heads/sdk-')) && github.repository == 'expo/expo' with: webhook: ${{ secrets.slack_webhook_ios }} channel: '#expo-ios' @@ -428,7 +428,7 @@ jobs: run: ccache -s -v - name: 🔔 Notify on Slack uses: ./.github/actions/slack-notify - if: failure() && (github.event.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/heads/sdk-')) + if: failure() && (github.event.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/heads/sdk-')) && github.repository == 'expo/expo' with: webhook: ${{ secrets.slack_webhook_android }} channel: '#expo-android' @@ -507,7 +507,7 @@ jobs: echo "Artifacts URL: ${{ steps.upload-artifacts.outputs.artifact-url }}" - name: 🔔 Notify on Slack uses: ./.github/actions/slack-notify - if: failure() && (github.event.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/heads/sdk-')) + if: failure() && (github.event.ref == 'refs/heads/main' || startsWith(github.event.ref, 'refs/heads/sdk-')) && github.repository == 'expo/expo' with: webhook: ${{ secrets.slack_webhook_android }} channel: '#expo-android' From 59e3b2ea67ec76fe3bf4a8c49635275e84cbd145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosmaty?= Date: Sat, 23 May 2026 10:59:43 +0200 Subject: [PATCH 2/4] [bare-expo] Add macrobenchmark (#46087) --- apps/bare-expo/App.tsx | 7 ++++ apps/bare-expo/android/app/build.gradle | 8 ++++ .../app/src/benchmark/AndroidManifest.xml | 7 ++++ .../java/dev/expo/payments/MainActivity.kt | 2 + apps/bare-expo/android/settings.gradle | 4 ++ apps/bare-expo/macrobenchmark/.gitignore | 1 + apps/bare-expo/macrobenchmark/build.gradle | 39 +++++++++++++++++++ .../src/main/AndroidManifest.xml | 6 +++ .../macrobenchmark/StartupBenchmark.kt | 35 +++++++++++++++++ .../benchmark-helper/android/build.gradle | 23 +++++++++++ .../benchmarkhelper/BenchmarkHelperModule.kt | 14 +++++++ .../benchmarkhelper/BenchmarkManager.kt | 18 +++++++++ .../benchmark-helper/expo-module.config.json | 6 +++ .../modules/benchmark-helper/package.json | 18 +++++++++ .../modules/benchmark-helper/src/index.ts | 13 +++++++ apps/bare-expo/tsconfig.json | 1 + 16 files changed, 202 insertions(+) create mode 100644 apps/bare-expo/android/app/src/benchmark/AndroidManifest.xml create mode 100644 apps/bare-expo/macrobenchmark/.gitignore create mode 100644 apps/bare-expo/macrobenchmark/build.gradle create mode 100644 apps/bare-expo/macrobenchmark/src/main/AndroidManifest.xml create mode 100644 apps/bare-expo/macrobenchmark/src/main/java/dev/expo/payments/macrobenchmark/StartupBenchmark.kt create mode 100644 apps/bare-expo/modules/benchmark-helper/android/build.gradle create mode 100644 apps/bare-expo/modules/benchmark-helper/android/src/main/java/expo/modules/benchmarkhelper/BenchmarkHelperModule.kt create mode 100644 apps/bare-expo/modules/benchmark-helper/android/src/main/java/expo/modules/benchmarkhelper/BenchmarkManager.kt create mode 100644 apps/bare-expo/modules/benchmark-helper/expo-module.config.json create mode 100644 apps/bare-expo/modules/benchmark-helper/package.json create mode 100644 apps/bare-expo/modules/benchmark-helper/src/index.ts diff --git a/apps/bare-expo/App.tsx b/apps/bare-expo/App.tsx index 8807fd85960173..be7dd0ea7d1283 100644 --- a/apps/bare-expo/App.tsx +++ b/apps/bare-expo/App.tsx @@ -1,4 +1,5 @@ import { ThemeProvider } from 'ThemeProvider'; +import BenchmarkHelper from 'benchmark-helper'; import { ObserveRoot } from 'expo-observe'; import * as Splashscreen from 'expo-splash-screen'; import React from 'react'; @@ -96,6 +97,12 @@ export default function Main() { const isLoaded = useLoaded(); + React.useEffect(() => { + if (isLoaded) { + BenchmarkHelper.reportFullyDrawn(); + } + }, [isLoaded]); + return ( {isLoaded ? : null} diff --git a/apps/bare-expo/android/app/build.gradle b/apps/bare-expo/android/app/build.gradle index e42b17eca0e9c4..955bc38ef55c79 100644 --- a/apps/bare-expo/android/app/build.gradle +++ b/apps/bare-expo/android/app/build.gradle @@ -125,6 +125,13 @@ android { minifyEnabled enableMinifyInReleaseBuilds proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } + benchmark { + initWith buildTypes.release + signingConfig signingConfigs.debug + minifyEnabled false + shrinkResources false + matchingFallbacks = ['release'] + } } packagingOptions { jniLibs { @@ -166,6 +173,7 @@ android { dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") + implementation expoLibs.androidx.profileinstaller def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true"; def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true"; diff --git a/apps/bare-expo/android/app/src/benchmark/AndroidManifest.xml b/apps/bare-expo/android/app/src/benchmark/AndroidManifest.xml new file mode 100644 index 00000000000000..96aad3409fc705 --- /dev/null +++ b/apps/bare-expo/android/app/src/benchmark/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/apps/bare-expo/android/app/src/main/java/dev/expo/payments/MainActivity.kt b/apps/bare-expo/android/app/src/main/java/dev/expo/payments/MainActivity.kt index 558e6ad29d6c1a..a72daaff89402c 100644 --- a/apps/bare-expo/android/app/src/main/java/dev/expo/payments/MainActivity.kt +++ b/apps/bare-expo/android/app/src/main/java/dev/expo/payments/MainActivity.kt @@ -9,10 +9,12 @@ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnable import com.facebook.react.defaults.DefaultReactActivityDelegate import expo.modules.ReactActivityDelegateWrapper +import expo.modules.benchmarkhelper.BenchmarkManager import expo.modules.splashscreen.SplashScreenManager class MainActivity : ReactActivity() { override fun onCreate(savedInstanceState: Bundle?) { + BenchmarkManager.delayFullDrawn(this) SplashScreenManager.registerOnActivity(this) super.onCreate(null) } diff --git a/apps/bare-expo/android/settings.gradle b/apps/bare-expo/android/settings.gradle index c8f09d4fedf31e..59eca01934d439 100644 --- a/apps/bare-expo/android/settings.gradle +++ b/apps/bare-expo/android/settings.gradle @@ -35,4 +35,8 @@ include(":expo-modules-test-core") project(":expo-modules-test-core").projectDir = new File("../../../packages/expo-modules-test-core/android") include ':app' + +include ':macrobenchmark' +project(':macrobenchmark').projectDir = new File('../macrobenchmark') + includeBuild(expoAutolinking.reactNativeGradlePlugin) diff --git a/apps/bare-expo/macrobenchmark/.gitignore b/apps/bare-expo/macrobenchmark/.gitignore new file mode 100644 index 00000000000000..796b96d1c40232 --- /dev/null +++ b/apps/bare-expo/macrobenchmark/.gitignore @@ -0,0 +1 @@ +/build diff --git a/apps/bare-expo/macrobenchmark/build.gradle b/apps/bare-expo/macrobenchmark/build.gradle new file mode 100644 index 00000000000000..1c6adb471be401 --- /dev/null +++ b/apps/bare-expo/macrobenchmark/build.gradle @@ -0,0 +1,39 @@ +apply plugin: 'com.android.test' +apply plugin: 'org.jetbrains.kotlin.android' + +android { + namespace 'dev.expo.payments.macrobenchmark' + compileSdk rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion + + defaultConfig { + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + testInstrumentationRunnerArguments['androidx.benchmark.suppressErrors'] = 'EMULATOR' + } + + buildTypes { + benchmark { + debuggable true + signingConfig debug.signingConfig + matchingFallbacks = ['release'] + } + } + + targetProjectPath ':app' + experimentalProperties['android.experimental.self-instrumenting'] = true +} + +dependencies { + implementation 'androidx.benchmark:benchmark-macro-junit4:1.4.1' + implementation expoLibs.androidx.junit + implementation expoLibs.androidx.espresso.core + implementation expoLibs.androidx.uiautomator +} + +androidComponents { + beforeVariants(selector().all()) { variant -> + variant.enable = variant.buildType == 'benchmark' + } +} diff --git a/apps/bare-expo/macrobenchmark/src/main/AndroidManifest.xml b/apps/bare-expo/macrobenchmark/src/main/AndroidManifest.xml new file mode 100644 index 00000000000000..fc89fb16f6510d --- /dev/null +++ b/apps/bare-expo/macrobenchmark/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/bare-expo/macrobenchmark/src/main/java/dev/expo/payments/macrobenchmark/StartupBenchmark.kt b/apps/bare-expo/macrobenchmark/src/main/java/dev/expo/payments/macrobenchmark/StartupBenchmark.kt new file mode 100644 index 00000000000000..79d8d411748306 --- /dev/null +++ b/apps/bare-expo/macrobenchmark/src/main/java/dev/expo/payments/macrobenchmark/StartupBenchmark.kt @@ -0,0 +1,35 @@ +package dev.expo.payments.macrobenchmark + +import android.content.Intent +import androidx.benchmark.macro.StartupMode +import androidx.benchmark.macro.StartupTimingMetric +import androidx.benchmark.macro.junit4.MacrobenchmarkRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.uiautomator.By +import androidx.test.uiautomator.Until +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class StartupBenchmark { + @get:Rule + val benchmarkRule = MacrobenchmarkRule() + + @Test + fun startupCold() = + benchmarkRule.measureRepeated( + packageName = "dev.expo.payments", + metrics = listOf(StartupTimingMetric()), + iterations = 5, + startupMode = StartupMode.COLD, + ) { + startActivityAndWait( + Intent(Intent.ACTION_MAIN).apply { + setClassName("dev.expo.payments", "dev.expo.payments.MainActivity") + } + ) + device.wait(Until.hasObject(By.text("Expo Test Suite")), 15_000) + device.waitForIdle() + } +} diff --git a/apps/bare-expo/modules/benchmark-helper/android/build.gradle b/apps/bare-expo/modules/benchmark-helper/android/build.gradle new file mode 100644 index 00000000000000..c0be20f1f12356 --- /dev/null +++ b/apps/bare-expo/modules/benchmark-helper/android/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'com.android.library' + id 'expo-module-gradle-plugin' +} + +group = 'host.exp.exponent' +version = '1.0.0' + +expoModule { + canBePublished false +} + +android { + namespace "expo.modules.benchmarkhelper" + defaultConfig { + versionCode 1 + versionName '1.0.0' + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.7.1' +} diff --git a/apps/bare-expo/modules/benchmark-helper/android/src/main/java/expo/modules/benchmarkhelper/BenchmarkHelperModule.kt b/apps/bare-expo/modules/benchmark-helper/android/src/main/java/expo/modules/benchmarkhelper/BenchmarkHelperModule.kt new file mode 100644 index 00000000000000..da10e51090358e --- /dev/null +++ b/apps/bare-expo/modules/benchmark-helper/android/src/main/java/expo/modules/benchmarkhelper/BenchmarkHelperModule.kt @@ -0,0 +1,14 @@ +package expo.modules.benchmarkhelper + +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition + +class BenchmarkHelperModule : Module() { + override fun definition() = ModuleDefinition { + Name("BenchmarkHelperModule") + + Function("reportFullyDrawn") { + BenchmarkManager.reportFullyDrawn(appContext.currentActivity) + } + } +} diff --git a/apps/bare-expo/modules/benchmark-helper/android/src/main/java/expo/modules/benchmarkhelper/BenchmarkManager.kt b/apps/bare-expo/modules/benchmark-helper/android/src/main/java/expo/modules/benchmarkhelper/BenchmarkManager.kt new file mode 100644 index 00000000000000..0d63071f44d7ab --- /dev/null +++ b/apps/bare-expo/modules/benchmark-helper/android/src/main/java/expo/modules/benchmarkhelper/BenchmarkManager.kt @@ -0,0 +1,18 @@ +package expo.modules.benchmarkhelper + +import android.app.Activity +import androidx.appcompat.app.AppCompatActivity + +object BenchmarkManager { + fun delayFullDrawn(activity: AppCompatActivity) { + activity.fullyDrawnReporter.addReporter() + } + + fun reportFullyDrawn(activity: Activity?) { + if (activity == null || activity !is AppCompatActivity) { + return + } + + activity.fullyDrawnReporter.removeReporter() + } +} diff --git a/apps/bare-expo/modules/benchmark-helper/expo-module.config.json b/apps/bare-expo/modules/benchmark-helper/expo-module.config.json new file mode 100644 index 00000000000000..6938ef35212278 --- /dev/null +++ b/apps/bare-expo/modules/benchmark-helper/expo-module.config.json @@ -0,0 +1,6 @@ +{ + "platforms": ["android"], + "android": { + "modules": ["expo.modules.benchmarkhelper.BenchmarkHelperModule"] + } +} diff --git a/apps/bare-expo/modules/benchmark-helper/package.json b/apps/bare-expo/modules/benchmark-helper/package.json new file mode 100644 index 00000000000000..01000c3905a8f4 --- /dev/null +++ b/apps/bare-expo/modules/benchmark-helper/package.json @@ -0,0 +1,18 @@ +{ + "name": "benchmark-helper", + "version": "0.0.1", + "private": true, + "description": "Helper module for Android macrobenchmark measurements", + "main": "src/index.ts", + "types": "src/index.ts", + "homepage": "https://expo.dev", + "author": "650 Industries, Inc.", + "license": "MIT", + "dependencies": {}, + "devDependencies": {}, + "peerDependencies": { + "expo": "workspace:*", + "react": "*", + "react-native": "*" + } +} diff --git a/apps/bare-expo/modules/benchmark-helper/src/index.ts b/apps/bare-expo/modules/benchmark-helper/src/index.ts new file mode 100644 index 00000000000000..e7e989d84588ce --- /dev/null +++ b/apps/bare-expo/modules/benchmark-helper/src/index.ts @@ -0,0 +1,13 @@ +import { NativeModule, Platform, requireNativeModule } from 'expo'; + +declare class BenchmarkHelperModule extends NativeModule { + reportFullyDrawn(): void; +} + +const noop: BenchmarkHelperModule = { + reportFullyDrawn() {}, +} as unknown as BenchmarkHelperModule; + +export default Platform.OS === 'android' + ? requireNativeModule('BenchmarkHelperModule') + : noop; diff --git a/apps/bare-expo/tsconfig.json b/apps/bare-expo/tsconfig.json index da5cff438c9b69..0a65bce415e453 100644 --- a/apps/bare-expo/tsconfig.json +++ b/apps/bare-expo/tsconfig.json @@ -5,6 +5,7 @@ "lib": ["dom", "esnext"], "paths": { "ThemeProvider": ["../common/ThemeProvider"], + "benchmark-helper": ["./modules/benchmark-helper"], "benchmarking": ["./modules/benchmarking"], "worklets-tester": ["./modules/worklets-tester"], "test-expo-ui": ["./modules/test-expo-ui"] From 311cb2902de0215efd4f63372ddde2ba25fcd92e Mon Sep 17 00:00:00 2001 From: Alan Hughes <30924086+alanjhughes@users.noreply.github.com> Date: Sat, 23 May 2026 10:04:31 +0100 Subject: [PATCH 3/4] [web][image-manipulator] Throw error when browser doesn't support requested image format (#46165) --- packages/expo-image-manipulator/CHANGELOG.md | 2 ++ .../build/web/ImageManipulatorImageRef.web.d.ts.map | 2 +- .../src/web/ImageManipulatorImageRef.web.ts | 10 +++++++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/expo-image-manipulator/CHANGELOG.md b/packages/expo-image-manipulator/CHANGELOG.md index c400b599c52938..42ebcf31bed0fd 100644 --- a/packages/expo-image-manipulator/CHANGELOG.md +++ b/packages/expo-image-manipulator/CHANGELOG.md @@ -8,6 +8,8 @@ ### 🐛 Bug fixes +- [Web] Throw an error when requested mime type is not supported. ([#46165](https://github.com/expo/expo/pull/46165) by [@alanjhughes](https://github.com/alanjhughes)) + ### 💡 Others ## 56.0.13 — 2026-05-21 diff --git a/packages/expo-image-manipulator/build/web/ImageManipulatorImageRef.web.d.ts.map b/packages/expo-image-manipulator/build/web/ImageManipulatorImageRef.web.d.ts.map index 89aed581658d70..27e5b5728683e4 100644 --- a/packages/expo-image-manipulator/build/web/ImageManipulatorImageRef.web.d.ts.map +++ b/packages/expo-image-manipulator/build/web/ImageManipulatorImageRef.web.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"ImageManipulatorImageRef.web.d.ts","sourceRoot":"","sources":["../../src/web/ImageManipulatorImageRef.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAEjC,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAI1E,MAAM,CAAC,OAAO,OAAO,wBAAyB,SAAQ,SAAS,CAAC,OAAO,CAAC;IACtE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAW;IAEzC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;gBAEvB,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB;IAMlD,IAAI,KAAK,WAER;IAED,IAAI,MAAM,WAET;IAEK,SAAS,CAAC,OAAO,GAAE,WAA+B,GAAG,OAAO,CAAC,WAAW,CAAC;CAsBhF"} \ No newline at end of file +{"version":3,"file":"ImageManipulatorImageRef.web.d.ts","sourceRoot":"","sources":["../../src/web/ImageManipulatorImageRef.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAEjC,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAI1E,MAAM,CAAC,OAAO,OAAO,wBAAyB,SAAQ,SAAS,CAAC,OAAO,CAAC;IACtE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAW;IAEzC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;gBAEvB,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB;IAMlD,IAAI,KAAK,WAER;IAED,IAAI,MAAM,WAET;IAEK,SAAS,CAAC,OAAO,GAAE,WAA+B,GAAG,OAAO,CAAC,WAAW,CAAC;CA8BhF"} \ No newline at end of file diff --git a/packages/expo-image-manipulator/src/web/ImageManipulatorImageRef.web.ts b/packages/expo-image-manipulator/src/web/ImageManipulatorImageRef.web.ts index 103cfa53c59a05..92f444786dd919 100644 --- a/packages/expo-image-manipulator/src/web/ImageManipulatorImageRef.web.ts +++ b/packages/expo-image-manipulator/src/web/ImageManipulatorImageRef.web.ts @@ -26,11 +26,19 @@ export default class ImageManipulatorImageRef extends SharedRef<'image'> { async saveAsync(options: SaveOptions = { base64: false }): Promise { return new Promise((resolve, reject) => { + const requestedType = `image/${options.format ?? SaveFormat.JPEG}`; this.canvas.toBlob( async (blob) => { if (!blob) { return reject(new Error(`Unable to save image: ${this.uri}`)); } + if (blob.type !== requestedType) { + return reject( + new Error( + `The browser does not support encoding "${requestedType}" images. Got "${blob.type}" instead. Try a different format like JPEG or PNG.` + ) + ); + } const base64 = options.base64 ? await blobToBase64String(blob) : undefined; const uri = URL.createObjectURL(blob); @@ -41,7 +49,7 @@ export default class ImageManipulatorImageRef extends SharedRef<'image'> { base64, }); }, - `image/${options.format ?? SaveFormat.JPEG}`, + requestedType, options.compress ); }); From ba34f955aead270e5dac8a76e601220bd61845c5 Mon Sep 17 00:00:00 2001 From: Nick Date: Sat, 23 May 2026 12:21:50 +0300 Subject: [PATCH 4/4] [expo-image][iOS] Fix loading images from the asset catalog (xcassets) (#46170) --- packages/expo-image/CHANGELOG.md | 3 ++ packages/expo-image/ios/ImageView.swift | 32 +++++++++++++++---- .../ios/Tests/ImageResizingSpec.swift | 8 +++++ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/expo-image/CHANGELOG.md b/packages/expo-image/CHANGELOG.md index 977a102bee3116..abe5a1760cea63 100644 --- a/packages/expo-image/CHANGELOG.md +++ b/packages/expo-image/CHANGELOG.md @@ -8,6 +8,9 @@ ### 🐛 Bug fixes +- [iOS] Fix `placeholder` failing to load images from the asset catalog (xcassets). ([#46170](https://github.com/expo/expo/pull/46170) by [@zhelezkov](https://github.com/zhelezkov)) +- [iOS] Fix xcasset images not loading from JS due to `file://` scheme mismatch. ([#46170](https://github.com/expo/expo/pull/46170) by [@zhelezkov](https://github.com/zhelezkov)) + ### 💡 Others ## 56.0.8 — 2026-05-21 diff --git a/packages/expo-image/ios/ImageView.swift b/packages/expo-image/ios/ImageView.swift index d7692899e8a4b0..cccedd7adf7328 100644 --- a/packages/expo-image/ios/ImageView.swift +++ b/packages/expo-image/ios/ImageView.swift @@ -352,9 +352,7 @@ public final class ImageView: ExpoView { } private func maybeRenderLocalAsset(from source: ImageSource) -> Bool { - let path = localAssetName(from: source.uri) - - if let path, let local = UIImage(named: path) { + if let local = localAssetImage(from: source) { renderSourceImage(local) return true } @@ -362,6 +360,13 @@ public final class ImageView: ExpoView { return false } + private func localAssetImage(from source: ImageSource) -> UIImage? { + guard let path = localAssetName(from: source.uri) else { + return nil + } + return UIImage(named: path) + } + // MARK: - Placeholder /** @@ -412,6 +417,14 @@ public final class ImageView: ExpoView { return } + // Asset catalog (xcassets) images aren't resolvable by SDWebImage, so try the + // local lookup first — mirroring the proper source path in `maybeRenderLocalAsset`. + if let localImage = localAssetImage(from: placeholder) { + placeholderImage = localImage + displayPlaceholderIfNecessary() + return + } + // Cache placeholders on the disk. Should we let the user choose whether // to cache them or apply the same policy as with the proper image? // Basically they are also cached in memory as the `placeholderImage` property, @@ -840,18 +853,23 @@ public final class ImageView: ExpoView { } func localAssetName(from url: URL?) -> String? { - guard let url, url.scheme == nil else { + guard let url else { return nil } - var path = url.path - guard !path.isEmpty else { + // `ExpoModulesCore` converts scheme-less URI strings from JS via + // `URL(fileURLWithPath:)`, so asset names like "my_image" arrive here with + // `scheme == "file"`. Accept those alongside truly scheme-less URLs; reject + // remote/SF Symbol/blurhash/etc. schemes. + if let scheme = url.scheme, scheme != "file" { return nil } + // Use `relativePath` so we recover the original input ("my_image") instead of + // the absolute path it resolves to against the file:// base + var path = url.relativePath if path.hasPrefix("/") { path.removeFirst() } - return path.isEmpty ? nil : path } diff --git a/packages/expo-image/ios/Tests/ImageResizingSpec.swift b/packages/expo-image/ios/Tests/ImageResizingSpec.swift index 2fc5c377737130..6e23568e02c9ca 100644 --- a/packages/expo-image/ios/Tests/ImageResizingSpec.swift +++ b/packages/expo-image/ios/Tests/ImageResizingSpec.swift @@ -137,6 +137,14 @@ class ImageResizingSpec: ExpoSpec { expect(localAssetName(from: URL(string: "/app_icon"))) == "app_icon" } + it("handles file URLs produced by ExpoModulesCore for scheme-less JS strings") { + // `ExpoModulesCore`'s `convertToUrl` wraps scheme-less JS strings via + // `URL(fileURLWithPath:)`, which is how asset names actually reach the native side. + expect(localAssetName(from: URL(fileURLWithPath: "app_icon"))) == "app_icon" + expect(localAssetName(from: URL(fileURLWithPath: "/app_icon"))) == "app_icon" + expect(localAssetName(from: URL(fileURLWithPath: "Images/MyIcon"))) == "Images/MyIcon" + } + it("ignores urls with a scheme") { expect(localAssetName(from: URL(string: "https://example.com/app_icon.png"))).to(beNil()) expect(localAssetName(from: URL(string: "sf:/star"))).to(beNil())