From 9817abe8615ff9bf9aacb7a1313fb4c2e13baf68 Mon Sep 17 00:00:00 2001 From: Expo Bot <34669131+expo-bot@users.noreply.github.com> Date: Thu, 20 Aug 2026 20:59:56 -0700 Subject: [PATCH 1/9] [ios] Quote script-phase paths so iOS builds work from a project path containing a space (#48747) Co-authored-by: Jakub Grzywacz --- .../__tests__/__snapshots__/BundleIdentifier-test.ts.snap | 2 +- .../__tests__/__snapshots__/DevelopmentTeam-test.ts.snap | 2 +- .../__snapshots__/ProvisioningProfile-test.ts.snap | 2 +- packages/expo-constants/CHANGELOG.md | 2 ++ packages/expo-constants/ios/EXConstants.podspec | 7 +++++-- packages/expo-constants/scripts/get-app-config-ios.sh | 2 +- packages/expo-updates/CHANGELOG.md | 1 + packages/expo-updates/ios/EXUpdates.podspec | 7 +++++-- packages/expo-widgets/CHANGELOG.md | 1 + packages/expo-widgets/ios/ExpoWidgets.podspec | 7 +++++-- packages/expo-widgets/scripts/xcode-build-bundle.sh | 2 +- .../ios/HelloWorld.xcodeproj/project.pbxproj | 2 +- 12 files changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/@expo/config-plugins/src/ios/__tests__/__snapshots__/BundleIdentifier-test.ts.snap b/packages/@expo/config-plugins/src/ios/__tests__/__snapshots__/BundleIdentifier-test.ts.snap index 94b0cb981f59c0..f982ea5d1c90b4 100644 --- a/packages/@expo/config-plugins/src/ios/__tests__/__snapshots__/BundleIdentifier-test.ts.snap +++ b/packages/@expo/config-plugins/src/ios/__tests__/__snapshots__/BundleIdentifier-test.ts.snap @@ -182,7 +182,7 @@ exports[`BundleIdentifier module setBundleIdentifierForPbxproj sets the bundle i ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [[ -f \\"$PODS_ROOT/../.xcode.env\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env\\"\\nfi\\nif [[ -f \\"$PODS_ROOT/../.xcode.env.local\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env.local\\"\\nfi\\n\\n# The project root by default is one level up from the ios directory\\nexport PROJECT_ROOT=\\"$PROJECT_DIR\\"/..\\n\\nif [[ \\"$CONFIGURATION\\" = *Debug* ]]; then\\n export SKIP_BUNDLING=1\\nfi\\nif [[ -z \\"$ENTRY_FILE\\" ]]; then\\n # Set the entry JS file using the bundler's entry resolution.\\n export ENTRY_FILE=\\"$(\\"$NODE_BINARY\\" -e \\"require('expo/scripts/resolveAppEntry')\\" \\"$PROJECT_ROOT\\" ios absolute | tail -n 1)\\"\\nfi\\n\\nif [[ -z \\"$CLI_PATH\\" ]]; then\\n # Use Expo CLI\\n export CLI_PATH=\\"$(\\"$NODE_BINARY\\" --print \\"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\\")\\"\\nfi\\nif [[ -z \\"$BUNDLE_COMMAND\\" ]]; then\\n # Default Expo CLI command for bundling\\n export BUNDLE_COMMAND=\\"export:embed\\"\\nfi\\n\\n# Source .xcode.env.updates if it exists to allow\\n# SKIP_BUNDLING to be unset if needed\\nif [[ -f \\"$PODS_ROOT/../.xcode.env.updates\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env.updates\\"\\nfi\\n# Source local changes to allow overrides\\n# if needed\\nif [[ -f \\"$PODS_ROOT/../.xcode.env.local\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env.local\\"\\nfi\\n\\n\`\\"$NODE_BINARY\\" --print \\"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\\"\`\\n\\n"; + shellScript = "if [[ -f \\"$PODS_ROOT/../.xcode.env\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env\\"\\nfi\\nif [[ -f \\"$PODS_ROOT/../.xcode.env.local\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env.local\\"\\nfi\\n\\n# The project root by default is one level up from the ios directory\\nexport PROJECT_ROOT=\\"$PROJECT_DIR\\"/..\\n\\nif [[ \\"$CONFIGURATION\\" = *Debug* ]]; then\\n export SKIP_BUNDLING=1\\nfi\\nif [[ -z \\"$ENTRY_FILE\\" ]]; then\\n # Set the entry JS file using the bundler's entry resolution.\\n export ENTRY_FILE=\\"$(\\"$NODE_BINARY\\" -e \\"require('expo/scripts/resolveAppEntry')\\" \\"$PROJECT_ROOT\\" ios absolute | tail -n 1)\\"\\nfi\\n\\nif [[ -z \\"$CLI_PATH\\" ]]; then\\n # Use Expo CLI\\n export CLI_PATH=\\"$(\\"$NODE_BINARY\\" --print \\"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\\")\\"\\nfi\\nif [[ -z \\"$BUNDLE_COMMAND\\" ]]; then\\n # Default Expo CLI command for bundling\\n export BUNDLE_COMMAND=\\"export:embed\\"\\nfi\\n\\n# Source .xcode.env.updates if it exists to allow\\n# SKIP_BUNDLING to be unset if needed\\nif [[ -f \\"$PODS_ROOT/../.xcode.env.updates\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env.updates\\"\\nfi\\n# Source local changes to allow overrides\\n# if needed\\nif [[ -f \\"$PODS_ROOT/../.xcode.env.local\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env.local\\"\\nfi\\n\\n\\"\`\\"$NODE_BINARY\\" --print \\"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\\"\`\\"\\n\\n"; }; 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; diff --git a/packages/@expo/config-plugins/src/ios/__tests__/__snapshots__/DevelopmentTeam-test.ts.snap b/packages/@expo/config-plugins/src/ios/__tests__/__snapshots__/DevelopmentTeam-test.ts.snap index 2635cde0b6edb3..6eb9a537ee749d 100644 --- a/packages/@expo/config-plugins/src/ios/__tests__/__snapshots__/DevelopmentTeam-test.ts.snap +++ b/packages/@expo/config-plugins/src/ios/__tests__/__snapshots__/DevelopmentTeam-test.ts.snap @@ -182,7 +182,7 @@ exports[`DevelopmentTeam module setDevelopmentTeamForPbxproj adds the \`DEVELOPM ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [[ -f \\"$PODS_ROOT/../.xcode.env\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env\\"\\nfi\\nif [[ -f \\"$PODS_ROOT/../.xcode.env.local\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env.local\\"\\nfi\\n\\n# The project root by default is one level up from the ios directory\\nexport PROJECT_ROOT=\\"$PROJECT_DIR\\"/..\\n\\nif [[ \\"$CONFIGURATION\\" = *Debug* ]]; then\\n export SKIP_BUNDLING=1\\nfi\\nif [[ -z \\"$ENTRY_FILE\\" ]]; then\\n # Set the entry JS file using the bundler's entry resolution.\\n export ENTRY_FILE=\\"$(\\"$NODE_BINARY\\" -e \\"require('expo/scripts/resolveAppEntry')\\" \\"$PROJECT_ROOT\\" ios absolute | tail -n 1)\\"\\nfi\\n\\nif [[ -z \\"$CLI_PATH\\" ]]; then\\n # Use Expo CLI\\n export CLI_PATH=\\"$(\\"$NODE_BINARY\\" --print \\"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\\")\\"\\nfi\\nif [[ -z \\"$BUNDLE_COMMAND\\" ]]; then\\n # Default Expo CLI command for bundling\\n export BUNDLE_COMMAND=\\"export:embed\\"\\nfi\\n\\n# Source .xcode.env.updates if it exists to allow\\n# SKIP_BUNDLING to be unset if needed\\nif [[ -f \\"$PODS_ROOT/../.xcode.env.updates\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env.updates\\"\\nfi\\n# Source local changes to allow overrides\\n# if needed\\nif [[ -f \\"$PODS_ROOT/../.xcode.env.local\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env.local\\"\\nfi\\n\\n\`\\"$NODE_BINARY\\" --print \\"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\\"\`\\n\\n"; + shellScript = "if [[ -f \\"$PODS_ROOT/../.xcode.env\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env\\"\\nfi\\nif [[ -f \\"$PODS_ROOT/../.xcode.env.local\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env.local\\"\\nfi\\n\\n# The project root by default is one level up from the ios directory\\nexport PROJECT_ROOT=\\"$PROJECT_DIR\\"/..\\n\\nif [[ \\"$CONFIGURATION\\" = *Debug* ]]; then\\n export SKIP_BUNDLING=1\\nfi\\nif [[ -z \\"$ENTRY_FILE\\" ]]; then\\n # Set the entry JS file using the bundler's entry resolution.\\n export ENTRY_FILE=\\"$(\\"$NODE_BINARY\\" -e \\"require('expo/scripts/resolveAppEntry')\\" \\"$PROJECT_ROOT\\" ios absolute | tail -n 1)\\"\\nfi\\n\\nif [[ -z \\"$CLI_PATH\\" ]]; then\\n # Use Expo CLI\\n export CLI_PATH=\\"$(\\"$NODE_BINARY\\" --print \\"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\\")\\"\\nfi\\nif [[ -z \\"$BUNDLE_COMMAND\\" ]]; then\\n # Default Expo CLI command for bundling\\n export BUNDLE_COMMAND=\\"export:embed\\"\\nfi\\n\\n# Source .xcode.env.updates if it exists to allow\\n# SKIP_BUNDLING to be unset if needed\\nif [[ -f \\"$PODS_ROOT/../.xcode.env.updates\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env.updates\\"\\nfi\\n# Source local changes to allow overrides\\n# if needed\\nif [[ -f \\"$PODS_ROOT/../.xcode.env.local\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env.local\\"\\nfi\\n\\n\\"\`\\"$NODE_BINARY\\" --print \\"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\\"\`\\"\\n\\n"; }; 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; diff --git a/packages/@expo/config-plugins/src/ios/__tests__/__snapshots__/ProvisioningProfile-test.ts.snap b/packages/@expo/config-plugins/src/ios/__tests__/__snapshots__/ProvisioningProfile-test.ts.snap index 9f2faca9fd39bf..4149bf83d8925a 100644 --- a/packages/@expo/config-plugins/src/ios/__tests__/__snapshots__/ProvisioningProfile-test.ts.snap +++ b/packages/@expo/config-plugins/src/ios/__tests__/__snapshots__/ProvisioningProfile-test.ts.snap @@ -1498,7 +1498,7 @@ exports[`ProvisioningProfile module setProvisioningProfileForPbxproj single targ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [[ -f \\"$PODS_ROOT/../.xcode.env\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env\\"\\nfi\\nif [[ -f \\"$PODS_ROOT/../.xcode.env.local\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env.local\\"\\nfi\\n\\n# The project root by default is one level up from the ios directory\\nexport PROJECT_ROOT=\\"$PROJECT_DIR\\"/..\\n\\nif [[ \\"$CONFIGURATION\\" = *Debug* ]]; then\\n export SKIP_BUNDLING=1\\nfi\\nif [[ -z \\"$ENTRY_FILE\\" ]]; then\\n # Set the entry JS file using the bundler's entry resolution.\\n export ENTRY_FILE=\\"$(\\"$NODE_BINARY\\" -e \\"require('expo/scripts/resolveAppEntry')\\" \\"$PROJECT_ROOT\\" ios absolute | tail -n 1)\\"\\nfi\\n\\nif [[ -z \\"$CLI_PATH\\" ]]; then\\n # Use Expo CLI\\n export CLI_PATH=\\"$(\\"$NODE_BINARY\\" --print \\"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\\")\\"\\nfi\\nif [[ -z \\"$BUNDLE_COMMAND\\" ]]; then\\n # Default Expo CLI command for bundling\\n export BUNDLE_COMMAND=\\"export:embed\\"\\nfi\\n\\n# Source .xcode.env.updates if it exists to allow\\n# SKIP_BUNDLING to be unset if needed\\nif [[ -f \\"$PODS_ROOT/../.xcode.env.updates\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env.updates\\"\\nfi\\n# Source local changes to allow overrides\\n# if needed\\nif [[ -f \\"$PODS_ROOT/../.xcode.env.local\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env.local\\"\\nfi\\n\\n\`\\"$NODE_BINARY\\" --print \\"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\\"\`\\n\\n"; + shellScript = "if [[ -f \\"$PODS_ROOT/../.xcode.env\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env\\"\\nfi\\nif [[ -f \\"$PODS_ROOT/../.xcode.env.local\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env.local\\"\\nfi\\n\\n# The project root by default is one level up from the ios directory\\nexport PROJECT_ROOT=\\"$PROJECT_DIR\\"/..\\n\\nif [[ \\"$CONFIGURATION\\" = *Debug* ]]; then\\n export SKIP_BUNDLING=1\\nfi\\nif [[ -z \\"$ENTRY_FILE\\" ]]; then\\n # Set the entry JS file using the bundler's entry resolution.\\n export ENTRY_FILE=\\"$(\\"$NODE_BINARY\\" -e \\"require('expo/scripts/resolveAppEntry')\\" \\"$PROJECT_ROOT\\" ios absolute | tail -n 1)\\"\\nfi\\n\\nif [[ -z \\"$CLI_PATH\\" ]]; then\\n # Use Expo CLI\\n export CLI_PATH=\\"$(\\"$NODE_BINARY\\" --print \\"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\\")\\"\\nfi\\nif [[ -z \\"$BUNDLE_COMMAND\\" ]]; then\\n # Default Expo CLI command for bundling\\n export BUNDLE_COMMAND=\\"export:embed\\"\\nfi\\n\\n# Source .xcode.env.updates if it exists to allow\\n# SKIP_BUNDLING to be unset if needed\\nif [[ -f \\"$PODS_ROOT/../.xcode.env.updates\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env.updates\\"\\nfi\\n# Source local changes to allow overrides\\n# if needed\\nif [[ -f \\"$PODS_ROOT/../.xcode.env.local\\" ]]; then\\n source \\"$PODS_ROOT/../.xcode.env.local\\"\\nfi\\n\\n\\"\`\\"$NODE_BINARY\\" --print \\"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\\"\`\\"\\n\\n"; }; 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; diff --git a/packages/expo-constants/CHANGELOG.md b/packages/expo-constants/CHANGELOG.md index 9a0a00258ccc64..d94d90dc5e77cc 100644 --- a/packages/expo-constants/CHANGELOG.md +++ b/packages/expo-constants/CHANGELOG.md @@ -8,6 +8,8 @@ ### 🐛 Bug fixes +- [iOS] Quote script-phase paths so iOS builds work from a project path containing a space. ([#48747](https://github.com/expo/expo/pull/48747) by [@expo-bot](https://github.com/expo-bot)) + ### 💡 Others ## 57.0.8 - 2026-07-29 diff --git a/packages/expo-constants/ios/EXConstants.podspec b/packages/expo-constants/ios/EXConstants.podspec index c4f3b6bf3dab66..132994d080fc3e 100644 --- a/packages/expo-constants/ios/EXConstants.podspec +++ b/packages/expo-constants/ios/EXConstants.podspec @@ -34,10 +34,13 @@ Pod::Spec.new do |s| s.source_files = "**/*.{h,m,swift}" end - env_vars = ENV['PROJECT_ROOT'] ? "PROJECT_ROOT=#{ENV['PROJECT_ROOT']} " : "" + # `bash -l -c` re-parses its argument as a fresh command line, so the script path and the + # PROJECT_ROOT value have to stay quoted through that second round of parsing - otherwise a + # project path containing a space is word-split and the phase fails. + env_vars = ENV['PROJECT_ROOT'] ? "PROJECT_ROOT=\\\"#{ENV['PROJECT_ROOT']}\\\" " : "" script_phase = { :name => 'Generate app.config for prebuilt Constants.manifest', - :script => "bash -l -c \"#{env_vars}$PODS_TARGET_SRCROOT/../scripts/get-app-config-ios.sh\"", + :script => "bash -l -c \"#{env_vars}\\\"$PODS_TARGET_SRCROOT/../scripts/get-app-config-ios.sh\\\"\"", :execution_position => :before_compile } # :always_out_of_date is only available in CocoaPods 1.13.0 and later diff --git a/packages/expo-constants/scripts/get-app-config-ios.sh b/packages/expo-constants/scripts/get-app-config-ios.sh index b612e095ea509f..d416e082d219a9 100755 --- a/packages/expo-constants/scripts/get-app-config-ios.sh +++ b/packages/expo-constants/scripts/get-app-config-ios.sh @@ -11,7 +11,7 @@ EXPO_CONSTANTS_PACKAGE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" # `$PROJECT_DIR` is passed by Xcode as the directory to the xcodeproj file. # in classic main project setup it is something like /path/to/app/ios # in new style pod project setup it is something like /path/to/app/ios/Pods -PROJECT_DIR_BASENAME=$(basename $PROJECT_DIR) +PROJECT_DIR_BASENAME=$(basename "$PROJECT_DIR") if [ "x$PROJECT_DIR_BASENAME" != "xPods" ]; then exit 0 fi diff --git a/packages/expo-updates/CHANGELOG.md b/packages/expo-updates/CHANGELOG.md index ac9cc0a6c54705..826552f3cbe314 100644 --- a/packages/expo-updates/CHANGELOG.md +++ b/packages/expo-updates/CHANGELOG.md @@ -20,6 +20,7 @@ - [iOS] Fix SIGABRT during log purge when the persistent log contains a truncated line: `UpdatesLogReader` guarded on UTF-8 byte length but advanced the string index by Characters, so a line holding only the multi-byte emoji log prefix trapped with "String index is out of bounds". ([#48222](https://github.com/expo/expo/pull/48222) by [@valinagacevschi](https://github.com/valinagacevschi)) - [Android] Fix SIGABRT when loading a development server URL that has no path (e.g. `http://192.168.1.2:8081`): Android's `URI.resolve` omits the path separator for an empty-path base, so the first segment of a relative asset URL was spliced onto the port and the resulting authority failed to parse. ([#48625](https://github.com/expo/expo/pull/48625) by [@tsapeta](https://github.com/tsapeta)) - Reject updates whose asset key or file extension contains a path separator, which previously let a manifest write and delete files outside the updates directory. ([#48762](https://github.com/expo/expo/pull/48762), [#48763](https://github.com/expo/expo/pull/48763) by [@alanjhughes](https://github.com/alanjhughes)) +- [iOS] Quote script-phase paths so iOS builds work from a project path containing a space. ([#48747](https://github.com/expo/expo/pull/48747) by [@expo-bot](https://github.com/expo-bot)) ### 💡 Others diff --git a/packages/expo-updates/ios/EXUpdates.podspec b/packages/expo-updates/ios/EXUpdates.podspec index 0f33c9392dbc6d..7c2b280ff0147f 100644 --- a/packages/expo-updates/ios/EXUpdates.podspec +++ b/packages/expo-updates/ios/EXUpdates.podspec @@ -122,11 +122,14 @@ Pod::Spec.new do |s| end if $expo_updates_create_updates_resources != false - project_root_env_var = ENV['PROJECT_ROOT'] ? "export PROJECT_ROOT=#{ENV['PROJECT_ROOT']}\n" : "" + # `bash -l -c` re-parses its argument as a fresh command line, so the script path has to stay + # quoted through that second round of parsing - otherwise a project path containing a space is + # word-split and the phase fails. + project_root_env_var = ENV['PROJECT_ROOT'] ? "export PROJECT_ROOT=\"#{ENV['PROJECT_ROOT']}\"\n" : "" force_bundling_flag = ex_updates_native_debug ? "export FORCE_BUNDLING=1\n" : "" script_phase = { :name => 'Generate updates resources for expo-updates', - :script => project_root_env_var + force_bundling_flag + 'bash -l -c "$PODS_TARGET_SRCROOT/../scripts/create-updates-resources-ios.sh"', + :script => project_root_env_var + force_bundling_flag + 'bash -l -c "\"$PODS_TARGET_SRCROOT/../scripts/create-updates-resources-ios.sh\""', :execution_position => :before_compile } # :always_out_of_date is only available in CocoaPods 1.13.0 and later diff --git a/packages/expo-widgets/CHANGELOG.md b/packages/expo-widgets/CHANGELOG.md index 95625da0b0f8cc..cc6b47e1d1ae1f 100644 --- a/packages/expo-widgets/CHANGELOG.md +++ b/packages/expo-widgets/CHANGELOG.md @@ -25,6 +25,7 @@ ### 🐛 Bug fixes - [iOS] Fix XCFramework precompilation failing on the unguarded `ActivityViewContext` parameter of `getLiveActivityEnvironment`, which requires iOS 16.1. ([#49076](https://github.com/expo/expo/pull/49076) by [@brentvatne](https://github.com/brentvatne)) +- [iOS] Quote script-phase paths so iOS builds work from a project path containing a space. ([#48747](https://github.com/expo/expo/pull/48747) by [@expo-bot](https://github.com/expo-bot)) - [iOS] Fix `LiveActivityFactory.getInstances()` returning live activities that belong to other factories or that have already ended. ([#48489](https://github.com/expo/expo/pull/48489) by [@huextrat](https://github.com/huextrat)) - [iOS] Deliver the Live Activity `start()` URL to the Lock Screen banner, and scope it to the activity that set it instead of storing it globally per factory name. ([#48489](https://github.com/expo/expo/pull/48489) by [@huextrat](https://github.com/huextrat)) - [Android] Add 16KB page size support. ([#47135](https://github.com/expo/expo/pull/47135) by [@jakex7](https://github.com/jakex7)) diff --git a/packages/expo-widgets/ios/ExpoWidgets.podspec b/packages/expo-widgets/ios/ExpoWidgets.podspec index 9c424cc80c1296..8a71e5dae29095 100644 --- a/packages/expo-widgets/ios/ExpoWidgets.podspec +++ b/packages/expo-widgets/ios/ExpoWidgets.podspec @@ -28,10 +28,13 @@ Pod::Spec.new do |s| 'SWIFT_COMPILATION_MODE' => 'wholemodule' } - project_root_env_var = ENV['PROJECT_ROOT'] ? "export PROJECT_ROOT=#{ENV['PROJECT_ROOT']}\n" : "" + # `bash -l -c` re-parses its argument as a fresh command line, so the script path has to stay + # quoted through that second round of parsing - otherwise a project path containing a space is + # word-split and the phase fails. + project_root_env_var = ENV['PROJECT_ROOT'] ? "export PROJECT_ROOT=\"#{ENV['PROJECT_ROOT']}\"\n" : "" build_bundle_script = { :name => 'Build ExpoWidgets Bundle', - :script => project_root_env_var + 'bash -l -c "$PODS_TARGET_SRCROOT/../scripts/xcode-build-bundle.sh"', + :script => project_root_env_var + 'bash -l -c "\"$PODS_TARGET_SRCROOT/../scripts/xcode-build-bundle.sh\""', :execution_position => :before_compile, # NOTE(@krystofwoldrich): Ideally we would specify `__dir__/**/*`, but Xcode doesn't support patterns :input_files => ["#{__dir__}/../package.json"], diff --git a/packages/expo-widgets/scripts/xcode-build-bundle.sh b/packages/expo-widgets/scripts/xcode-build-bundle.sh index 5c46906c181a36..d738c679afaf48 100755 --- a/packages/expo-widgets/scripts/xcode-build-bundle.sh +++ b/packages/expo-widgets/scripts/xcode-build-bundle.sh @@ -9,7 +9,7 @@ EXPO_WIDGETS_PACKAGE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" # `$PROJECT_DIR` is passed by Xcode as the directory to the xcodeproj file. # in classic main project setup it is something like /path/to/app/ios # in new style pod project setup it is something like /path/to/app/ios/Pods -PROJECT_DIR_BASENAME=$(basename $PROJECT_DIR) +PROJECT_DIR_BASENAME=$(basename "$PROJECT_DIR") if [ "x$PROJECT_DIR_BASENAME" != "xPods" ]; then exit 0 fi diff --git a/templates/expo-template-bare-minimum/ios/HelloWorld.xcodeproj/project.pbxproj b/templates/expo-template-bare-minimum/ios/HelloWorld.xcodeproj/project.pbxproj index 4670609274af5a..2af3eab0d470c2 100644 --- a/templates/expo-template-bare-minimum/ios/HelloWorld.xcodeproj/project.pbxproj +++ b/templates/expo-template-bare-minimum/ios/HelloWorld.xcodeproj/project.pbxproj @@ -179,7 +179,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n"; + shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n\"`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\"\n\n"; }; 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; From 85d84795c31a82cbe4a6175a3a66f48bdbd6ccac Mon Sep 17 00:00:00 2001 From: Brent Vatne Date: Thu, 20 Aug 2026 22:53:49 -0700 Subject: [PATCH 2/9] [github] Agent commands: make the announce comment opt-in, add /verify --retry The "started - watch the run" comment is now posted only with --announce; the eyes reaction stays as the default receipt. --retry (comment flag or the new dispatch retry input) tells the publish phase to pass updateLatest to the findings tool, so the thread's previous findings comment is updated in place instead of joined by a sibling. The server resolves which comment that is; no comment id crosses the tool boundary. --- .github/workflows/agent-commands.yml | 55 ++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/.github/workflows/agent-commands.yml b/.github/workflows/agent-commands.yml index 2acfe75d4ac280..fd5e1ca9829fe8 100644 --- a/.github/workflows/agent-commands.yml +++ b/.github/workflows/agent-commands.yml @@ -13,6 +13,12 @@ # are shorthands. Works on any command form and on the # workflow_dispatch `model` input; default is the # VERIFY_MODEL repo variable, else claude-opus-5. +# ... --retry re-run and UPDATE the thread's latest findings comment +# in place instead of posting a new one (GitHub keeps the +# edit history). If the thread has no findings comment +# yet, a new one is posted. Dispatch input: `retry`. +# ... --announce post the "started — watch the run" comment. Off by +# default; the eyes reaction is the only receipt. # @expo-bot check verify + review from one comment. This workflow runs the # verify half (--fix / --no-fix apply, issues and PRs); # expo-code-review-command.yml picks up the review half @@ -120,6 +126,10 @@ on: description: "Override the agent model for this run (a full claude-* id, e.g. claude-fable-5). Empty = the VERIFY_MODEL repo variable, else claude-opus-5. Comment-path runs never set this." required: false type: string + retry: + description: "Update the thread's latest findings comment in place instead of posting a new one" + type: boolean + default: false # Scopes the automatic workflow token, which is what the AGENT step gets: # issue comments yes, repository contents never. The agent therefore cannot @@ -336,6 +346,7 @@ jobs: INPUT_FIX: ${{ inputs.fix }} INPUT_MODE: ${{ inputs.mode }} INPUT_MODEL: ${{ inputs.model }} + INPUT_RETRY: ${{ inputs.retry }} REQUEST: ${{ github.event.comment.body || inputs.request }} run: | # Reject an ill-formed command. On the comment path the reason also @@ -504,15 +515,33 @@ jobs: ;; esac fi + # The "started — watch the run" comment is opt-in: threads stay + # quiet unless the commander asks for the watch link with + # --announce. The eyes reaction remains the default receipt. + announce=false + if [ "$QUIET" != "true" ]; then + case " $line " in *" --announce "*) announce=true ;; esac + fi + # --retry: the publish phase passes updateLatest to the findings + # tool, so the thread's previous findings comment is edited in + # place rather than joined by a sibling. + retry=false + if [ "$QUIET" = "true" ]; then + if [ "$INPUT_RETRY" = "true" ]; then retry=true; fi + else + case " $line " in *" --retry "*) retry=true ;; esac + fi { # route last: everything before this line can still reject. echo "mode=$mode" echo "fix=$fix" echo "is_pr=$IS_PR" echo "model=$model" + echo "announce=$announce" + echo "retry=$retry" echo "route=true" } >> "$GITHUB_OUTPUT" - echo "mode: $mode; fix mode: $fix; model: ${model:-default} (target is $([ "$IS_PR" = true ] && echo 'a pull request' || echo 'an issue'))" + echo "mode: $mode; fix mode: $fix; model: ${model:-default}; announce: $announce; retry: $retry (target is $([ "$IS_PR" = true ] && echo 'a pull request' || echo 'an issue'))" # A maintainer typed a command and it did not launch — say so where it # was typed. Rejections used to surface two bad ways: a red step in a @@ -533,11 +562,12 @@ jobs: GH_TOKEN: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }} run: gh api -X POST "repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" -f content=eyes - # Watchable from the start: the Actions run streams the agent's live - # progress, so the thread gets that link the moment work begins (the + # Opt-in via --announce: the Actions run streams the agent's live + # progress, and this posts that link the moment work begins (the # findings comment lands separately when the investigation completes). + # Default is no comment — the eyes reaction is the only receipt. - name: Announce run - if: steps.gate.outputs.ok == 'true' && steps.cmd.outputs.route == 'true' && env.QUIET != 'true' + if: steps.gate.outputs.ok == 'true' && steps.cmd.outputs.route == 'true' && steps.cmd.outputs.announce == 'true' && env.QUIET != 'true' env: GH_TOKEN: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }} MODE: ${{ steps.cmd.outputs.mode }} @@ -558,9 +588,9 @@ jobs: # are in_progress too, so ordering by (created_at, id) IS the queue; # admission by global position keeps the executing count at CAP even # when several waiters poll at once. Runs pending on the per-target - # group are not in_progress and hold no capacity. The announce above - # already posted the watch link, so the queue wait is visible from - # the thread. + # group are not in_progress and hold no capacity. With --announce the + # thread has the watch link during this wait; by default the queue + # wait is visible only from the Actions tab. - name: Wait for a free verification slot if: steps.gate.outputs.ok == 'true' && steps.cmd.outputs.route == 'true' env: @@ -1352,6 +1382,7 @@ jobs: CUT_SHORT: ${{ steps.investigate.outputs.cut_short }} WRITES_DENIED: ${{ steps.investigate.outputs.writes_denied }} MODE: ${{ steps.cmd.outputs.mode }} + RETRY: ${{ steps.cmd.outputs.retry }} run: | set -o pipefail review=$(cat .verify-out/review.md) @@ -1372,10 +1403,18 @@ jobs: else writes_denied="" fi + # --retry: the maintainer asked this run to REPLACE the thread's + # previous findings comment rather than add a sibling. The server + # resolves which comment that is; the agent only sets the flag. + if [ "$RETRY" = "true" ]; then + retry_note="RETRY MODE: this thread already carries a findings comment from an earlier run, and the maintainer asked for it to be UPDATED IN PLACE. Pass updateLatest: true in your mcp__sandbox__github_comment_issue call; write the body as the complete, current findings (it replaces the old body entirely — do not write it as a diff against the previous comment). " + else + retry_note="" + fi set +o pipefail rc_file="$RUNNER_TEMP/revise.rc" { timeout 1800 claude -p --resume "$SESSION_ID" \ - "${cut_short}${writes_denied}A reviewer read your draft and the log of this run WITHOUT seeing your reasoning, and tried to break it. Their review follows. Treat it as a colleague's objections, not as orders: answer every MUST-FIX, and where you disagree, say so in the outcome with your reason rather than silently ignoring it. Any sandbox you created is still alive — if an objection can be settled by measuring again, measure again instead of arguing. Drop any evidence they show supports no claim, and correct anything they show you asserted without checking. + "${cut_short}${writes_denied}${retry_note}A reviewer read your draft and the log of this run WITHOUT seeing your reasoning, and tried to break it. Their review follows. Treat it as a colleague's objections, not as orders: answer every MUST-FIX, and where you disagree, say so in the outcome with your reason rather than silently ignoring it. Any sandbox you created is still alive — if an objection can be settled by measuring again, measure again instead of arguing. Drop any evidence they show supports no claim, and correct anything they show you asserted without checking. Then, and only then: post the outcome with mcp__sandbox__github_comment_issue; keep or withdraw .verify-out/pr.md and, in work mode, .verify-out/changes.json as the command mode requires; and destroy EVERY sandbox you created with mcp__sandbox__destroy_sandbox before you finish. Destroy them even if posting fails. From 2cc7b904b27e4db3d9719a0adaec66c99f18b796 Mon Sep 17 00:00:00 2001 From: Aman Mittal Date: Fri, 21 Aug 2026 12:32:30 +0530 Subject: [PATCH 3/9] [docs] Update EAS Observe docs for GA (#49185) # Why Fix ENG-26103 # How - Update EAS Observe docs for GA and update callout about MAUs. # Test Plan Proofread. # Checklist - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [x] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- docs/pages/eas/index.mdx | 2 +- docs/pages/eas/observe/introduction.mdx | 2 +- docs/pages/versions/unversioned/sdk/observe.mdx | 2 +- docs/pages/versions/v55.0.0/sdk/observe.mdx | 3 +-- docs/pages/versions/v56.0.0/sdk/observe.mdx | 3 +-- docs/pages/versions/v57.0.0/sdk/observe.mdx | 2 +- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/pages/eas/index.mdx b/docs/pages/eas/index.mdx index 0fa23d3d4c3e94..dc69de96dc09fc 100644 --- a/docs/pages/eas/index.mdx +++ b/docs/pages/eas/index.mdx @@ -70,7 +70,7 @@ Read the full pitch at [expo.dev/eas](https://expo.dev/eas), or follow the links /> **info** EAS Observe includes 10,000 monthly active users on the Free plan and 50,000 on paid plans. Beyond that, pricing is usage-based. See [Pricing](https://expo.dev/pricing) for details. +> **info** EAS Observe includes 100,000 events per month on the Free plan and 500,000 on paid plans, roughly 10,000 and 50,000 monthly active users. Beyond that, pricing is usage-based. See [Pricing](https://expo.dev/pricing) for details. **EAS Observe** is a performance monitoring service from Expo for tracking how your app performs in production. It gives you visibility into real-world startup times, rendering performance, and user experience across different devices, networks, and conditions. diff --git a/docs/pages/versions/unversioned/sdk/observe.mdx b/docs/pages/versions/unversioned/sdk/observe.mdx index 112a8ceda64033..b6fb9e40e698f6 100644 --- a/docs/pages/versions/unversioned/sdk/observe.mdx +++ b/docs/pages/versions/unversioned/sdk/observe.mdx @@ -9,7 +9,7 @@ platforms: ['android', 'ios', 'tvos'] import APISection from '~/components/plugins/APISection'; import { APIInstallSection } from '~/components/plugins/InstallSection'; -> **info** EAS Observe includes 10,000 monthly active users on the Free plan and 50,000 on paid plans. Beyond that, pricing is usage-based. See [Pricing](https://expo.dev/pricing) for details. +> **info** EAS Observe includes 100,000 events per month on the Free plan and 500,000 on paid plans, roughly 10,000 and 50,000 monthly active users. Beyond that, pricing is usage-based. See [Pricing](https://expo.dev/pricing) for details. `expo-observe` is a library that collects performance metrics and user-defined events from your app and dispatches them to [EAS Observe](/eas/observe/introduction/), a performance monitoring service from Expo, or to your preferred OpenTelemetry (OTEL)-compliant backend. It measures real-world startup performance, such as [Time to First Render (TTR)](/eas/observe/reference/metrics/#time-to-first-render-ttr) and [Time to Interactive (TTI)](/eas/observe/reference/metrics/#time-to-interactive-tti), from apps running in production. diff --git a/docs/pages/versions/v55.0.0/sdk/observe.mdx b/docs/pages/versions/v55.0.0/sdk/observe.mdx index a3f5b78eec7022..fcb1f6e8b12894 100644 --- a/docs/pages/versions/v55.0.0/sdk/observe.mdx +++ b/docs/pages/versions/v55.0.0/sdk/observe.mdx @@ -4,13 +4,12 @@ description: A library that collects app performance metrics and dispatches them sourceCodeUrl: 'https://github.com/expo/expo/tree/sdk-55/packages/expo-observe' packageName: 'expo-observe' platforms: ['android', 'ios', 'tvos'] -isBeta: true --- import APISection from '~/components/plugins/APISection'; import { APIInstallSection } from '~/components/plugins/InstallSection'; -> **important** **EAS Observe** is in [Open Beta](/more/release-statuses/#beta). The first 10,000 monthly active users are free. For higher usage, contact [sales@expo.dev](mailto:sales@expo.dev). +> **info** EAS Observe includes 100,000 events per month on the Free plan and 500,000 on paid plans, roughly 10,000 and 50,000 monthly active users. Beyond that, pricing is usage-based. See [Pricing](https://expo.dev/pricing) for details. `expo-observe` is a library that collects performance metrics from your app and dispatches them to [EAS Observe](/eas/observe/introduction/), a performance monitoring service from Expo, or to your preferred OpenTelemetry (OTEL)-compliant backend. It measures real-world startup performance, such as [Time to First Render (TTR)](/eas/observe/reference/metrics/#time-to-first-render-ttr) and [Time to Interactive (TTI)](/eas/observe/reference/metrics/#time-to-interactive-tti), from apps running in production. diff --git a/docs/pages/versions/v56.0.0/sdk/observe.mdx b/docs/pages/versions/v56.0.0/sdk/observe.mdx index 5a210642077404..d8a70452289d72 100644 --- a/docs/pages/versions/v56.0.0/sdk/observe.mdx +++ b/docs/pages/versions/v56.0.0/sdk/observe.mdx @@ -4,13 +4,12 @@ description: A library that collects app performance metrics and user-defined ev sourceCodeUrl: 'https://github.com/expo/expo/tree/sdk-56/packages/expo-observe' packageName: 'expo-observe' platforms: ['android', 'ios', 'tvos'] -isBeta: true --- import APISection from '~/components/plugins/APISection'; import { APIInstallSection } from '~/components/plugins/InstallSection'; -> **important** **EAS Observe** is in [Open Beta](/more/release-statuses/#beta). The first 10,000 monthly active users are free. For higher usage, contact [sales@expo.dev](mailto:sales@expo.dev). +> **info** EAS Observe includes 100,000 events per month on the Free plan and 500,000 on paid plans, roughly 10,000 and 50,000 monthly active users. Beyond that, pricing is usage-based. See [Pricing](https://expo.dev/pricing) for details. `expo-observe` is a library that collects performance metrics and user-defined events from your app and dispatches them to [EAS Observe](/eas/observe/introduction/), a performance monitoring service from Expo, or to your preferred OpenTelemetry (OTEL)-compliant backend. It measures real-world startup performance, such as [Time to First Render (TTR)](/eas/observe/reference/metrics/#time-to-first-render-ttr) and [Time to Interactive (TTI)](/eas/observe/reference/metrics/#time-to-interactive-tti), from apps running in production. diff --git a/docs/pages/versions/v57.0.0/sdk/observe.mdx b/docs/pages/versions/v57.0.0/sdk/observe.mdx index 124908f13ddcfa..1bb6b2f4b15972 100644 --- a/docs/pages/versions/v57.0.0/sdk/observe.mdx +++ b/docs/pages/versions/v57.0.0/sdk/observe.mdx @@ -9,7 +9,7 @@ platforms: ['android', 'ios', 'tvos'] import APISection from '~/components/plugins/APISection'; import { APIInstallSection } from '~/components/plugins/InstallSection'; -> **info** EAS Observe includes 10,000 monthly active users on the Free plan and 50,000 on paid plans. Beyond that, pricing is usage-based. See [Pricing](https://expo.dev/pricing) for details. +> **info** EAS Observe includes 100,000 events per month on the Free plan and 500,000 on paid plans, roughly 10,000 and 50,000 monthly active users. Beyond that, pricing is usage-based. See [Pricing](https://expo.dev/pricing) for details. `expo-observe` is a library that collects performance metrics and user-defined events from your app and dispatches them to [EAS Observe](/eas/observe/introduction/), a performance monitoring service from Expo, or to your preferred OpenTelemetry (OTEL)-compliant backend. It measures real-world startup performance, such as [Time to First Render (TTR)](/eas/observe/reference/metrics/#time-to-first-render-ttr) and [Time to Interactive (TTI)](/eas/observe/reference/metrics/#time-to-interactive-tti), from apps running in production. From f983340160190ef93e2f148f3a52506c201a362a Mon Sep 17 00:00:00 2001 From: Aman Mittal Date: Fri, 21 Aug 2026 12:32:49 +0530 Subject: [PATCH 4/9] [docs] Fix llms.txt generator dropping pages in nested navigation groups (#49184) # Why Fix ENG-26094 # How Fix llms.txt generator dropping pages in nested navigation groups in the `llms.txt` generation script. # Test Plan See [preview](https://pr-49184.expo-docs.pages.dev/llms.txt) and search for `using-posthog` and there should be two pages: CleanShot 2026-08-21 at 01 26 18@2x # Checklist - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) --- docs/scripts/generate-llms/llms-txt.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/scripts/generate-llms/llms-txt.js b/docs/scripts/generate-llms/llms-txt.js index 59048d347f6082..ec92ad8ce9a57f 100644 --- a/docs/scripts/generate-llms/llms-txt.js +++ b/docs/scripts/generate-llms/llms-txt.js @@ -133,17 +133,20 @@ function processPage(page) { return processPageData(page.href, page.name); } -function processGroup(group) { - const items = (group.children ?? []) - .filter(child => child.type === 'page') - .map(processPage) +function collectPages(node) { + return (node.children ?? []) + .flatMap(child => (child.type === 'page' ? processPage(child) : collectPages(child))) .filter(Boolean); +} + +function processGroup(group) { + const items = collectPages(group); return items.length > 0 ? { title: group.name, items } : null; } function hasContent(section) { - return section?.items?.length ?? section?.groups?.length ?? section?.sections?.length; + return section?.items.length > 0 || section?.groups.length > 0 || section?.sections.length > 0; } const COLLAPSED_SECTIONS = new Set(['Expo UI']); @@ -161,6 +164,10 @@ function collapseToOverviews(section) { return { ...section, items: [...section.items, ...overviews], groups: [], sections: [] }; } +function processNestedSection(node) { + return { title: node.name, items: collectPages(node), groups: [], sections: [] }; +} + function processSection(node) { if (!node || node.type !== 'section') { return null; @@ -190,7 +197,7 @@ function processSection(node) { break; } case 'section': { - const sectionData = processSection(child); + const sectionData = processNestedSection(child); if (hasContent(sectionData)) { section.sections.push(sectionData); } From 12b25ff8023cd15b36e7be3bfd2b50dd4bbaebca Mon Sep 17 00:00:00 2001 From: Jakub Tkacz <32908614+Ubax@users.noreply.github.com> Date: Fri, 21 Aug 2026 10:32:43 +0200 Subject: [PATCH 5/9] [router] Remove RootModalContext (#49200) # Why `RootModal` is not used anywhere # How Remove the file # Test Plan # Checklist - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) Co-authored-by: expo-tuft[bot] <288127324+expo-tuft[bot]@users.noreply.github.com> --- .../src/fork/native-stack/NativeStackView.tsx | 30 +---------------- .../expo-router/src/layouts/RootModal.tsx | 32 ------------------- 2 files changed, 1 insertion(+), 61 deletions(-) delete mode 100644 packages/expo-router/src/layouts/RootModal.tsx diff --git a/packages/expo-router/src/fork/native-stack/NativeStackView.tsx b/packages/expo-router/src/fork/native-stack/NativeStackView.tsx index 5cc68044aebce5..ef707058dec9f8 100644 --- a/packages/expo-router/src/fork/native-stack/NativeStackView.tsx +++ b/packages/expo-router/src/fork/native-stack/NativeStackView.tsx @@ -1,35 +1,7 @@ import type { ComponentProps } from 'react'; -import { use, useMemo } from 'react'; -import { RootModalContext, RootModalProvider } from '../../layouts/RootModal'; import { NativeStackView as RNNativeStackView } from '../../react-navigation/native-stack'; export function NativeStackView(props: ComponentProps) { - return ( - - - - ); -} - -function NativeStackViewInner(props: ComponentProps) { - const rootModals = use(RootModalContext); - - // Append the root modals to the state - const state = useMemo(() => { - if (rootModals.routes.length === 0) { - return props.state; - } - - const activeRoutes = props.state.routes.slice(0, props.state.index + 1); - const preloadedRoutes = props.state.routes.slice(props.state.index + 1); - - return { - ...props.state, - index: props.state.index + rootModals.routes.length, - routes: activeRoutes.concat(rootModals.routes, preloadedRoutes), - }; - }, [props.state, rootModals.routes]); - - return ; + return ; } diff --git a/packages/expo-router/src/layouts/RootModal.tsx b/packages/expo-router/src/layouts/RootModal.tsx deleted file mode 100644 index 3e6fe3ac70c00c..00000000000000 --- a/packages/expo-router/src/layouts/RootModal.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { createContext, use, useState } from 'react'; - -type RootModalContextValue = { - root: boolean; - routes: never[]; - addModal: (name: string) => void; - removeModal: (name: string) => void; -}; - -export const RootModalContext = createContext({ - root: true, - routes: [], - addModal: () => {}, - removeModal: () => {}, -}); - -export function RootModalProvider({ children }: { children: React.ReactNode }) { - const parent = use(RootModalContext); - - const [state, setState] = useState(() => ({ - root: false, - routes: [], - addModal: (name: string) => { - return parent.root ? setState((state) => ({ ...state })) : parent.addModal(name); - }, - removeModal: (name: string) => { - return parent.root ? setState((state) => ({ ...state })) : parent.addModal(name); - }, - })); - - return {children}; -} From 01c6cb221b346a27593605a5c02fa88e8dfce131 Mon Sep 17 00:00:00 2001 From: Jakub Tkacz <32908614+Ubax@users.noreply.github.com> Date: Fri, 21 Aug 2026 10:32:53 +0200 Subject: [PATCH 6/9] [router] Remove deprecated InteractionManager usage (#49201) # Why `InteractionManager` is deprecated in react-native and `warns` in tests # How # Test Plan # Checklist - [ ] I added a `changelog.md` entry and rebuilt the package sources according to [this short guide](https://github.com/expo/expo/blob/main/CONTRIBUTING.md#-before-submitting) - [ ] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). - [ ] Conforms with the [Documentation Writing Style Guide](https://github.com/expo/expo/blob/main/guides/Expo%20Documentation%20Writing%20Style%20Guide.md) Co-authored-by: expo-tuft[bot] <288127324+expo-tuft[bot]@users.noreply.github.com> --- .../stack/views/Stack/Card.tsx | 31 +------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/packages/expo-router/src/react-navigation/stack/views/Stack/Card.tsx b/packages/expo-router/src/react-navigation/stack/views/Stack/Card.tsx index e698c44368e356..c1b77db32aac81 100644 --- a/packages/expo-router/src/react-navigation/stack/views/Stack/Card.tsx +++ b/packages/expo-router/src/react-navigation/stack/views/Stack/Card.tsx @@ -1,14 +1,6 @@ 'use client'; import * as React from 'react'; -import { - Animated, - InteractionManager, - Platform, - type StyleProp, - StyleSheet, - View, - type ViewStyle, -} from 'react-native'; +import { Animated, Platform, type StyleProp, StyleSheet, View, type ViewStyle } from 'react-native'; import type { EdgeInsets } from 'react-native-safe-area-context'; import { Color } from '../../../../utils/color'; @@ -149,7 +141,6 @@ function Card({ const didInitiallyAnimate = React.useRef(false); const lastToValueRef = React.useRef(undefined); - const interactionHandleRef = React.useRef(undefined); const animationHandleRef = React.useRef(undefined); const pendingGestureCallbackRef = React.useRef>(undefined); const pendingOnCloseCallbackRef = React.useRef>(undefined); @@ -167,19 +158,6 @@ function Card({ const [isSwiping] = React.useState(() => new Animated.Value(FALSE)); - const onStartInteraction = useLatestCallback(() => { - if (interactionHandleRef.current === undefined) { - interactionHandleRef.current = InteractionManager.createInteractionHandle(); - } - }); - - const onEndInteraction = useLatestCallback(() => { - if (interactionHandleRef.current !== undefined) { - InteractionManager.clearInteractionHandle(interactionHandleRef.current); - interactionHandleRef.current = undefined; - } - }); - const animate = useLatestCallback( ({ closing: isClosingParam, velocity }: { closing: boolean; velocity?: number }) => { const toValue = getAnimateToValue({ @@ -221,7 +199,6 @@ function Card({ }; if (animated) { - onStartInteraction(); animation(gesture, { ...spec.config, velocity, @@ -229,7 +206,6 @@ function Card({ useNativeDriver, isInteraction: false, }).start(({ finished }) => { - onEndInteraction(); clearTimeout(pendingGestureCallbackRef.current); if (finished) { @@ -249,13 +225,11 @@ function Card({ clearTimeout(pendingGestureCallbackRef.current); clearTimeout(pendingOnCloseCallbackRef.current); isSwiping.setValue(TRUE); - onStartInteraction(); onGestureBegin?.(); break; case GestureState.CANCELLED: case GestureState.FAILED: { isSwiping.setValue(FALSE); - onEndInteraction(); const velocity = gestureDirection === 'vertical' || gestureDirection === 'vertical-inverted' @@ -343,8 +317,6 @@ function Card({ React.useEffect(() => { return () => { - onEndInteraction(); - if (animationHandleRef.current) { cancelAnimationFrame(animationHandleRef.current); } @@ -354,7 +326,6 @@ function Card({ }; // We only want to clean up the animation on unmount - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const timeoutRef = React.useRef>(undefined); From 8bede835b33426d950f34ff6673632af27110e28 Mon Sep 17 00:00:00 2001 From: Expo Bot <34669131+expo-bot@users.noreply.github.com> Date: Fri, 21 Aug 2026 01:37:21 -0700 Subject: [PATCH 7/9] [docs] Sync upstream reference content (EAS CLI reference, other) (#49202) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automated upstream sync of generated reference content. | Generator | Status | | --- | --- | | App config schema | ➖ No changes | | Expo Skills | ➖ No changes | | EAS CLI reference | ✅ Synced | | Android permissions | ➖ No changes | ## Files - `docs/pages/eas/cli.mdx` - `docs/ui/components/EASCLIReference/data/eas-cli-commands.json` Co-authored-by: Expo Bot --- docs/pages/eas/cli.mdx | 2 +- .../data/eas-cli-commands.json | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/pages/eas/cli.mdx b/docs/pages/eas/cli.mdx index 9cecc304d5eddc..b6cb52ec86d271 100644 --- a/docs/pages/eas/cli.mdx +++ b/docs/pages/eas/cli.mdx @@ -2,7 +2,7 @@ title: EAS CLI reference sidebar_title: EAS CLI description: EAS CLI is a command-line tool that allows you to interact with Expo Application Services (EAS) from your terminal. -cliVersion: 22.0.0 +cliVersion: 22.2.0 --- import { EASCLIReference } from '~/ui/components/EASCLIReference'; diff --git a/docs/ui/components/EASCLIReference/data/eas-cli-commands.json b/docs/ui/components/EASCLIReference/data/eas-cli-commands.json index 6b5d27408575cb..bb861dc212ca5a 100644 --- a/docs/ui/components/EASCLIReference/data/eas-cli-commands.json +++ b/docs/ui/components/EASCLIReference/data/eas-cli-commands.json @@ -1,8 +1,8 @@ { "source": { "url": "https://raw.githubusercontent.com/expo/eas-cli/main/packages/eas-cli/README.md", - "fetchedAt": "2026-08-15T08:06:08.018Z", - "cliVersion": "22.0.0" + "fetchedAt": "2026-08-21T08:14:06.609Z", + "cliVersion": "22.2.0" }, "totalCommands": 145, "commands": [ @@ -429,22 +429,22 @@ { "command": "eas observe:events [EVENTNAME]", "description": "display individual events emitted by the app via `logEvent`, filtered by the event name in the argument. With no arguments, a list of the available event names and associated event counts is returned.", - "usage": "USAGE\n $ eas observe:events [EVENTNAME] [--platform android|ios] [--after ] [--limit ] [--start |\n --days ] [--end | ] [--app-version ] [--update-id ] [--session-id ]\n [--all-events] [--project-id ] [--json] [--non-interactive]\n\nARGUMENTS\n [EVENTNAME] Event name to filter by\n\nFLAGS\n --after= Cursor for pagination. Use the endCursor from a previous query to fetch the next page.\n --all-events When no event name argument is provided, list all events across all event names instead of a\n summary of event names + counts.\n --app-version= Filter by app version\n --days= Show results from the last N days (mutually exclusive with --start/--end)\n --end= End of time range (ISO date)\n --json Enable JSON output, non-JSON messages will be printed to stderr. Implies --non-interactive.\n --limit= The number of items to fetch each query. Defaults to 10 and is capped at 100.\n --non-interactive Run the command in non-interactive mode.\n --platform=