diff --git a/.github/workflows/test-suite-brownfield-isolated.yml b/.github/workflows/test-suite-brownfield-isolated.yml
index f3ecace4b08667..9f61f006ac0bfb 100644
--- a/.github/workflows/test-suite-brownfield-isolated.yml
+++ b/.github/workflows/test-suite-brownfield-isolated.yml
@@ -206,7 +206,7 @@ jobs:
- name: 🍏 Build iOS artifacts (apps/brownfield-tester/expo-app)
run: |
npx expo prebuild --clean -p ios
- npx expo-brownfield build:ios --${{ matrix.build-type }} --verbose -a ../../../artifacts
+ npx expo-brownfield build:ios --${{ matrix.build-type }} --verbose -a ../../../artifacts -p BrownfieldPackage
working-directory: apps/brownfield-tester/expo-app
- name: 💾 Save ccache
if: always()
@@ -214,7 +214,7 @@ jobs:
with:
path: ${{ runner.temp }}/.ccache
key: ${{ steps.ccache-restore.outputs.cache-primary-key }}
- - name: 🔨 Add XCFrameworks to SwiftUI project
+ - name: 🔨 Add brownfield Swift Package to the app
run: ruby packages/expo-brownfield/e2e/scripts/add_xcframeworks.rb
- name: 🍺 Install Maestro
run: |
diff --git a/apps/bare-expo/e2e/expo-video/playback-test.yaml b/apps/bare-expo/e2e/expo-video/playback-test.yaml
index 871755cba9eebf..c780f57e19f321 100644
--- a/apps/bare-expo/e2e/expo-video/playback-test.yaml
+++ b/apps/bare-expo/e2e/expo-video/playback-test.yaml
@@ -8,7 +8,7 @@ jsEngine: graaljs
- assertVisible: 'source = Big Buck Bunny'
- assertVisible: 'isPlaying = false'
- assertVisible: 'isAtStart = true'
-- assertVisible: 'duration = 596'
+- assertVisible: 'duration = 634'
- assertVisible: 'currentTime = 0'
- assertVisible: 'mimeType = video/avc'
- assertVisible: 'isSupported = true'
diff --git a/apps/bare-expo/e2e/expo-video/screenshots/pip-1/pip-view.base.android.png b/apps/bare-expo/e2e/expo-video/screenshots/pip-1/pip-view.base.android.png
index 84f17163f5e00e..b7b80173a91dc5 100644
Binary files a/apps/bare-expo/e2e/expo-video/screenshots/pip-1/pip-view.base.android.png and b/apps/bare-expo/e2e/expo-video/screenshots/pip-1/pip-view.base.android.png differ
diff --git a/apps/bare-expo/e2e/expo-video/screenshots/playback-test-1/video-view.base.android.png b/apps/bare-expo/e2e/expo-video/screenshots/playback-test-1/video-view.base.android.png
index d8e17b62e9f583..a9593351fcd2ee 100644
Binary files a/apps/bare-expo/e2e/expo-video/screenshots/playback-test-1/video-view.base.android.png and b/apps/bare-expo/e2e/expo-video/screenshots/playback-test-1/video-view.base.android.png differ
diff --git a/apps/bare-expo/e2e/expo-video/screenshots/playback-test-1/video-view.base.ios.png b/apps/bare-expo/e2e/expo-video/screenshots/playback-test-1/video-view.base.ios.png
index c15e422c0719a3..e47564e9bd6183 100644
Binary files a/apps/bare-expo/e2e/expo-video/screenshots/playback-test-1/video-view.base.ios.png and b/apps/bare-expo/e2e/expo-video/screenshots/playback-test-1/video-view.base.ios.png differ
diff --git a/apps/bare-expo/e2e/expo-video/screenshots/player-output-1/view-player-output.base.android.png b/apps/bare-expo/e2e/expo-video/screenshots/player-output-1/view-player-output.base.android.png
index c4f6e170788d68..7b7da57bd8ba2d 100644
Binary files a/apps/bare-expo/e2e/expo-video/screenshots/player-output-1/view-player-output.base.android.png and b/apps/bare-expo/e2e/expo-video/screenshots/player-output-1/view-player-output.base.android.png differ
diff --git a/apps/bare-expo/e2e/expo-video/screenshots/player-output-1/view-player-output.base.ios.png b/apps/bare-expo/e2e/expo-video/screenshots/player-output-1/view-player-output.base.ios.png
index ac73f44415ce4c..696f2e69e20f25 100644
Binary files a/apps/bare-expo/e2e/expo-video/screenshots/player-output-1/view-player-output.base.ios.png and b/apps/bare-expo/e2e/expo-video/screenshots/player-output-1/view-player-output.base.ios.png differ
diff --git a/apps/bare-expo/e2e/expo-video/screenshots/player-output-2/view-player-output.base.android.png b/apps/bare-expo/e2e/expo-video/screenshots/player-output-2/view-player-output.base.android.png
index 6ef080f50863f6..7d5962d3606b32 100644
Binary files a/apps/bare-expo/e2e/expo-video/screenshots/player-output-2/view-player-output.base.android.png and b/apps/bare-expo/e2e/expo-video/screenshots/player-output-2/view-player-output.base.android.png differ
diff --git a/apps/bare-expo/e2e/expo-video/screenshots/player-output-2/view-player-output.base.ios.png b/apps/bare-expo/e2e/expo-video/screenshots/player-output-2/view-player-output.base.ios.png
index 1fc2985feb49f6..7adaf7a7c091b3 100644
Binary files a/apps/bare-expo/e2e/expo-video/screenshots/player-output-2/view-player-output.base.ios.png and b/apps/bare-expo/e2e/expo-video/screenshots/player-output-2/view-player-output.base.ios.png differ
diff --git a/apps/bare-expo/e2e/expo-video/screenshots/player-output-4/view-player-output.base.android.png b/apps/bare-expo/e2e/expo-video/screenshots/player-output-4/view-player-output.base.android.png
index 9f0537517f528c..1d9d6e2b2ba79f 100644
Binary files a/apps/bare-expo/e2e/expo-video/screenshots/player-output-4/view-player-output.base.android.png and b/apps/bare-expo/e2e/expo-video/screenshots/player-output-4/view-player-output.base.android.png differ
diff --git a/apps/bare-expo/e2e/expo-video/screenshots/player-output-4/view-player-output.base.ios.png b/apps/bare-expo/e2e/expo-video/screenshots/player-output-4/view-player-output.base.ios.png
index f6f09c73f1dd60..4bfc469a33140d 100644
Binary files a/apps/bare-expo/e2e/expo-video/screenshots/player-output-4/view-player-output.base.ios.png and b/apps/bare-expo/e2e/expo-video/screenshots/player-output-4/view-player-output.base.ios.png differ
diff --git a/apps/bare-expo/e2e/expo-video/screenshots/surface-type-1/surface-type-test.base.android.png b/apps/bare-expo/e2e/expo-video/screenshots/surface-type-1/surface-type-test.base.android.png
index 103489b492e246..6cdcb4c082024c 100644
Binary files a/apps/bare-expo/e2e/expo-video/screenshots/surface-type-1/surface-type-test.base.android.png and b/apps/bare-expo/e2e/expo-video/screenshots/surface-type-1/surface-type-test.base.android.png differ
diff --git a/apps/brownfield-tester/expo-app/app.json b/apps/brownfield-tester/expo-app/app.json
index efb848dd2c5f74..21367bac72939a 100644
--- a/apps/brownfield-tester/expo-app/app.json
+++ b/apps/brownfield-tester/expo-app/app.json
@@ -37,7 +37,14 @@
}
}
],
- "expo-brownfield"
+ [
+ "expo-brownfield",
+ {
+ "ios": {
+ "usePrebuiltReactNative": true
+ }
+ }
+ ]
],
"experiments": {
"typedRoutes": true,
diff --git a/apps/native-component-list/src/screens/Audio/AudioControlsScreen.tsx b/apps/native-component-list/src/screens/Audio/AudioControlsScreen.tsx
index bda50992a9a231..2fa41818ae0d1a 100644
--- a/apps/native-component-list/src/screens/Audio/AudioControlsScreen.tsx
+++ b/apps/native-component-list/src/screens/Audio/AudioControlsScreen.tsx
@@ -17,8 +17,7 @@ const artworkUrl1 =
'https://images.unsplash.com/photo-1549138144-42ff3cdd2bf8?q=80&w=3504&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D';
const artworkUrl2 =
'https://images.unsplash.com/photo-1549228167-511375f69159?q=80&w=3676&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D';
-const remoteSource =
- 'https://p.scdn.co/mp3-preview/f7a8ab9c5768009b65a30e9162555e8f21046f46?cid=162b7dc01f3a4a2ca32ed3cec83d1e02';
+const remoteSource = 'https://expo-test-media.com/audio/por_una_cabeza.mp3';
const localSource = require('../../../assets/sounds/polonez.mp3');
enum LockScreenButton {
diff --git a/apps/native-component-list/src/screens/Audio/AudioPlayer.tsx b/apps/native-component-list/src/screens/Audio/AudioPlayer.tsx
index 664514d9d33c85..9c26984f234e22 100644
--- a/apps/native-component-list/src/screens/Audio/AudioPlayer.tsx
+++ b/apps/native-component-list/src/screens/Audio/AudioPlayer.tsx
@@ -13,8 +13,7 @@ type AudioPlayerProps = {
};
const localSource = require('../../../assets/sounds/polonez.mp3');
-const remoteSource =
- 'https://p.scdn.co/mp3-preview/f7a8ab9c5768009b65a30e9162555e8f21046f46?cid=162b7dc01f3a4a2ca32ed3cec83d1e02';
+const remoteSource = 'https://expo-test-media.com/audio/por_una_cabeza.mp3';
export default function AudioPlayer({
source,
diff --git a/apps/native-component-list/src/screens/Audio/AudioPlayerScreen.tsx b/apps/native-component-list/src/screens/Audio/AudioPlayerScreen.tsx
index 2e0fec7cd3e62d..1cfca3fef0c551 100644
--- a/apps/native-component-list/src/screens/Audio/AudioPlayerScreen.tsx
+++ b/apps/native-component-list/src/screens/Audio/AudioPlayerScreen.tsx
@@ -24,7 +24,7 @@ export default function AudioScreen(props: any) {
HTTP player
Remote asset with downloadFirst
}) {
diff --git a/apps/native-component-list/src/screens/Video/videoSources.ts b/apps/native-component-list/src/screens/Video/videoSources.ts
index d613af08c85c51..c8829137abe22d 100644
--- a/apps/native-component-list/src/screens/Video/videoSources.ts
+++ b/apps/native-component-list/src/screens/Video/videoSources.ts
@@ -17,38 +17,47 @@ export const seekOptimizedSource: VideoSource = {
metadata: {
title: 'Tola running (seek optimized)',
artist:
- "This video has been optimized for seeking by exoirting all of it's frames as keyframes",
+ "This video has been optimized for seeking by exporting all of it's frames as keyframes",
},
};
+// Fallback: https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
const bigBuckBunnySource: VideoSource = {
- // backup at https://github.com/vonovak/expo-video-tests/releases/tag/v0
- uri: 'https://archive.org/download/BigBuckBunny_124/Content/big_buck_bunny_720p_surround.mp4',
+ uri: 'https://expo-test-media.com/big_buck_bunny/bbb_720p.mp4',
metadata: {
title: 'Big Buck Bunny',
artist: 'The Open Movie Project',
- artwork:
- 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/c5/Big_buck_bunny_poster_big.jpg/1200px-Big_buck_bunny_poster_big.jpg',
+ artwork: 'https://expo-test-media.com/big_buck_bunny/artwork.jpg',
},
};
+// Fallback: https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4
const elephantsDreamSource: VideoSource = {
- // backup at https://github.com/vonovak/expo-video-tests/releases/tag/v0
- uri: 'https://archive.org/download/ElephantsDream/ed_1024.mp4',
+ uri: 'https://expo-test-media.com/elephants_dream/ed_720p.mp4',
metadata: {
title: 'Elephants Dream',
artist: 'Blender Foundation',
- artwork: 'https://upload.wikimedia.org/wikipedia/commons/0/0c/ElephantsDreamPoster.jpg',
+ artwork: 'https://expo-test-media.com/elephants_dream/artwork.jpg',
},
};
+// Fallback https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/hls/TearsOfSteel.m3u8
export const hlsSource: VideoSource = {
- uri: 'https://devstreaming-cdn.apple.com/videos/streaming/examples/adv_dv_atmos/main.m3u8',
+ uri: 'https://expo-test-media.com/tos_hls/master.m3u8',
metadata: {
- title: 'Becoming You Trailer',
- artist: 'Apple',
- artwork:
- 'https://www.apple.com/tv-pr/shows-and-films/b/becoming-you/images/show-home-graphic-header/4x1/Apple_TV_Becoming_You_key_art_graphic_header_4_1_show_home.jpg.og.jpg?1659052681724',
+ title: 'Tears Of Steel',
+ artist: 'Blender Foundation',
+ artwork: 'https://expo-test-media.com/tos_hls/artwork.jpg',
+ },
+};
+
+// Fallback: https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/dash/TearsOfSteel.mpd
+export const dashSource: VideoSource = {
+ uri: 'https://expo-test-media.com/tos_dash/manifest.mpd',
+ metadata: {
+ title: 'Tears Of Steel',
+ artist: 'Blender Foundation',
+ artwork: 'https://expo-test-media.com/tos_dash/artwork.jpg',
},
};
@@ -103,14 +112,18 @@ const forBiggerBlazesSource: VideoSource = {
// source: https://reference.dashif.org/dash.js/latest/samples/drm/widevine.html
const androidDrmSource: VideoSource = {
- uri: 'https://bitmovin-a.akamaihd.net/content/sintel/hls/playlist.m3u8',
+ uri: 'https://expo-test-media.com/tos_widevine/manifest.mpd',
+ drm: {
+ licenseServer: 'https://cwip-shaka-proxy.appspot.com/no_auth',
+ type: 'widevine',
+ },
};
const videoLabels: string[] = [
'Big Buck Bunny',
'Elephants Dream',
'For Bigger Blazes',
- 'Becoming You (HLS)',
+ 'Tears Of Steel (HLS)',
'Cute Doggo (local video)',
'Null Source',
'Audio Track',
diff --git a/docs/pages/additional-resources/index.mdx b/docs/pages/additional-resources/index.mdx
index b21aae78176689..17d4a6baa8eed4 100644
--- a/docs/pages/additional-resources/index.mdx
+++ b/docs/pages/additional-resources/index.mdx
@@ -1,5 +1,5 @@
---
-modificationDate: February 2, 2026
+modificationDate: March 2, 2026
title: Additional resources
description: A reference of resources that are useful to learn about Expo tooling and services.
---
diff --git a/docs/pages/deploy/build-project.mdx b/docs/pages/deploy/build-project.mdx
index 37b9033167558b..bb49e24e8b8223 100644
--- a/docs/pages/deploy/build-project.mdx
+++ b/docs/pages/deploy/build-project.mdx
@@ -123,7 +123,7 @@ The workflow above will create Android and iOS builds on every commit to your pr
-Learn more about common patterns with the [workflows examples guide](/eas/workflows/examples).
+Learn more about common patterns with the [workflows examples guide](/eas/workflows/examples/introduction).
## Release builds locally
diff --git a/docs/pages/deploy/send-over-the-air-updates.mdx b/docs/pages/deploy/send-over-the-air-updates.mdx
index 55944bf38b1d0b..bd4251e707f821 100644
--- a/docs/pages/deploy/send-over-the-air-updates.mdx
+++ b/docs/pages/deploy/send-over-the-air-updates.mdx
@@ -50,7 +50,7 @@ The workflow above will send an over-the-air update for the `production` update
-Learn more about common patterns with the [workflows examples guide](/eas/workflows/examples).
+Learn more about common patterns with the [workflows examples guide](/eas/workflows/examples/introduction).
## Learn more
diff --git a/docs/pages/deploy/submit-to-app-stores.mdx b/docs/pages/deploy/submit-to-app-stores.mdx
index 32222ac29ca98f..c70580e743ee71 100644
--- a/docs/pages/deploy/submit-to-app-stores.mdx
+++ b/docs/pages/deploy/submit-to-app-stores.mdx
@@ -159,7 +159,7 @@ The workflow above will create Android and iOS builds on every commit to your pr
-Learn more about common patterns with the [workflows examples guide](/eas/workflows/examples).
+Learn more about common patterns with the [workflows examples guide](/eas/workflows/examples/introduction).
## Manual submission to app stores
diff --git a/docs/pages/deploy/web.mdx b/docs/pages/deploy/web.mdx
index 8ad545e4bafe48..f197f6aa90c011 100644
--- a/docs/pages/deploy/web.mdx
+++ b/docs/pages/deploy/web.mdx
@@ -61,7 +61,7 @@ The workflow above will create a web deployment on every commit to your project'
-Learn more about common patterns with the [workflows examples guide](/eas/workflows/examples).
+Learn more about common patterns with the [workflows examples guide](/eas/workflows/examples/introduction).
## Learn more
diff --git a/docs/pages/eas/workflows/automating-eas-cli.mdx b/docs/pages/eas/workflows/automating-eas-cli.mdx
index 4772a0928a7005..679f678305ea9b 100644
--- a/docs/pages/eas/workflows/automating-eas-cli.mdx
+++ b/docs/pages/eas/workflows/automating-eas-cli.mdx
@@ -119,7 +119,7 @@ You can provide parameters to update specific branches or channels, and configur
Workflows are a powerful way to automate your development and release processes. Learn how to create development builds, publish preview updates, and create production builds with the workflows examples guide:
-Learn more about common patterns with the [workflows examples guide](/eas/workflows/examples).
+Learn more about common patterns with the [workflows examples guide](/eas/workflows/examples/introduction).
## Learn more
diff --git a/docs/pages/versions/v55.0.0/sdk/navigation-bar.mdx b/docs/pages/versions/v55.0.0/sdk/navigation-bar.mdx
index bed1b04a693d67..0393f9250ef7d1 100644
--- a/docs/pages/versions/v55.0.0/sdk/navigation-bar.mdx
+++ b/docs/pages/versions/v55.0.0/sdk/navigation-bar.mdx
@@ -16,8 +16,6 @@ import {
`expo-navigation-bar` enables you to modify and observe the native navigation bar on Android devices. Due to some Android platform restrictions, parts of this API overlap with the `expo-status-bar` API.
-Properties are named after style properties; visibility, position, backgroundColor, borderColor, and so on.
-
The APIs in this package have no impact when "Gesture Navigation" is enabled on the Android device. There is currently no native Android API to detect if "Gesture Navigation" is enabled or not.
## Installation
@@ -37,12 +35,9 @@ You can configure `expo-navigation-bar` using its built-in [config plugin](/conf
[
"expo-navigation-bar",
{
- "backgroundColor": "#0f172a",
+ "enforceContrast": true,
"barStyle": "light",
- "borderColor": "#1f2937",
- "visibility": "visible",
- "behavior": "inset-swipe",
- "position": "relative"
+ "visibility": "visible"
}
]
]
@@ -55,10 +50,10 @@ You can configure `expo-navigation-bar` using its built-in [config plugin](/conf
If you're not using Continuous Native Generation ([CNG](/workflow/continuous-native-generation/)) or you're using a native **android** project manually, then you need to add the following configuration to your native project:
-- To apply `backgroundColor` to the navigation bar, add `navigationBarColor` to **android/app/src/main/res/values/colors.xml**:
+- To apply `visibility` to the navigation bar, add `expo_navigation_bar_visibility` to **android/app/src/main/res/values/strings.xml**:
```xml
- #0f172a
-
- ```
-
- Then, apply `android:navigationBarColor` to **android/app/src/main/res/values/styles.xml**:
-
- ```xml
-
- ```
-
-- To apply `borderColor`, `visibility`, `position`, and `behavior` to the navigation bar, add `expo_navigation_bar_border_color`, `expo_navigation_bar_visibility`, `expo_navigation_bar_position`, and `expo_navigation_bar_behavior` to **android/app/src/main/res/values/strings.xml**:
-
- ```xml
-
-
-
- -14735049
visible
- relative
- inset-swipe
-
- ```
-
-- To apply `legacyVisible` to the navigation bar, add `expo_navigation_bar_legacy_visible` to **android/app/src/main/res/values/strings.xml**:
-
- ```xml
-
-
- immersive
```
diff --git a/docs/public/static/talks.ts b/docs/public/static/talks.ts
index 69b8b1fb1386c5..06a4f057981235 100644
--- a/docs/public/static/talks.ts
+++ b/docs/public/static/talks.ts
@@ -437,6 +437,12 @@ export const LIVE_STREAMS = [
] as Talk[];
export const YOUTUBE_VIDEOS = [
+ {
+ title: "What's new in Expo SDK 55",
+ event: 'Expo Tutorials',
+ videoId: 'q72aeXsbF9c',
+ uploadDate: '2026-02-26',
+ },
{
title: 'AI mobile app development with Replit and Expo',
event: 'Expo Tutorials',
diff --git a/packages/expo-brownfield/CHANGELOG.md b/packages/expo-brownfield/CHANGELOG.md
index d74eeefdd6e1ce..fe8f588a3c47f8 100644
--- a/packages/expo-brownfield/CHANGELOG.md
+++ b/packages/expo-brownfield/CHANGELOG.md
@@ -8,6 +8,7 @@
- [android] add basic implementation of shared state for android ([#43097](https://github.com/expo/expo/pull/43097) by [@pmleczek](https://github.com/pmleczek))
- [cli] allow shipping ios artifacts as swift package ([#43369](https://github.com/expo/expo/pull/43369) by [@pmleczek](https://github.com/pmleczek))
+- [ios] enable optional usage of prebuilt RN frameworks ([#43356](https://github.com/expo/expo/pull/43356) by [@pmleczek](https://github.com/pmleczek))
### 🐛 Bug fixes
diff --git a/packages/expo-brownfield/cli/build/commands/build-ios.js b/packages/expo-brownfield/cli/build/commands/build-ios.js
index fef8df0ba8ffa8..dd2563b2727303 100644
--- a/packages/expo-brownfield/cli/build/commands/build-ios.js
+++ b/packages/expo-brownfield/cli/build/commands/build-ios.js
@@ -5,11 +5,14 @@ const buildIos = async (command) => {
await (0, utils_1.validatePrebuild)('ios');
const config = (0, utils_1.resolveBuildConfigIos)(command.opts());
(0, utils_1.printIosConfig)(config);
- await (0, utils_1.cleanUpArtifacts)(config);
- (0, utils_1.makeArtifactsDirectory)(config);
await (0, utils_1.buildFramework)(config);
- await (0, utils_1.createSwiftPackage)(config);
- await (0, utils_1.createXcframework)(config);
- await (0, utils_1.copyHermesXcframework)(config);
+ if (config.output !== 'frameworks') {
+ // Ship frameworks as swift package
+ (0, utils_1.shipSwiftPackage)(config);
+ }
+ else {
+ // Ship frameworks as standalone XCFrameworks
+ (0, utils_1.shipFrameworks)(config);
+ }
};
exports.default = buildIos;
diff --git a/packages/expo-brownfield/cli/build/utils/config.js b/packages/expo-brownfield/cli/build/utils/config.js
index 5cf331e74f2622..7888657bfe0d64 100644
--- a/packages/expo-brownfield/cli/build/utils/config.js
+++ b/packages/expo-brownfield/cli/build/utils/config.js
@@ -43,8 +43,7 @@ const resolveBuildConfigIos = (options) => {
derivedDataPath,
device,
simulator,
- hermesFrameworkPath,
- scheme,
+ scheme: resolveScheme(options),
workspace: resolveWorkspace(options),
};
};
diff --git a/packages/expo-brownfield/cli/build/utils/constants.d.ts b/packages/expo-brownfield/cli/build/utils/constants.d.ts
new file mode 100644
index 00000000000000..4f26dcc2baf424
--- /dev/null
+++ b/packages/expo-brownfield/cli/build/utils/constants.d.ts
@@ -0,0 +1,2 @@
+import type { XCFrameworkSpec } from './types';
+export declare const XCFramework: Record;
diff --git a/packages/expo-brownfield/cli/build/utils/constants.js b/packages/expo-brownfield/cli/build/utils/constants.js
new file mode 100644
index 00000000000000..89fd2f8f5506e2
--- /dev/null
+++ b/packages/expo-brownfield/cli/build/utils/constants.js
@@ -0,0 +1,20 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.XCFramework = void 0;
+exports.XCFramework = {
+ Hermes: {
+ path: 'ios/Pods/hermes-engine/destroot/Library/Frameworks/universal/hermesvm.xcframework',
+ name: 'hermesvm',
+ targets: ['hermesvm'],
+ },
+ React: {
+ path: 'ios/Pods/React-Core-prebuilt/React.xcframework',
+ name: 'React',
+ targets: ['React'],
+ },
+ ReactDependencies: {
+ path: 'ios/Pods/ReactNativeDependencies/framework/packages/react-native/ReactNativeDependencies.xcframework',
+ name: 'ReactNativeDependencies',
+ targets: ['ReactNativeDependencies'],
+ },
+};
diff --git a/packages/expo-brownfield/cli/build/utils/index.d.ts b/packages/expo-brownfield/cli/build/utils/index.d.ts
index d47b08a1c809ce..608f8369ebd348 100644
--- a/packages/expo-brownfield/cli/build/utils/index.d.ts
+++ b/packages/expo-brownfield/cli/build/utils/index.d.ts
@@ -1,5 +1,6 @@
export * from './android';
export * from './commands';
+export * from './constants';
export * from './config';
export { default as CLIError } from './error';
export * from './ios';
diff --git a/packages/expo-brownfield/cli/build/utils/index.js b/packages/expo-brownfield/cli/build/utils/index.js
index af6047af8cd081..c609b836e9c88b 100644
--- a/packages/expo-brownfield/cli/build/utils/index.js
+++ b/packages/expo-brownfield/cli/build/utils/index.js
@@ -20,6 +20,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.CLIError = void 0;
__exportStar(require("./android"), exports);
__exportStar(require("./commands"), exports);
+__exportStar(require("./constants"), exports);
__exportStar(require("./config"), exports);
var error_1 = require("./error");
Object.defineProperty(exports, "CLIError", { enumerable: true, get: function () { return __importDefault(error_1).default; } });
diff --git a/packages/expo-brownfield/cli/build/utils/ios.d.ts b/packages/expo-brownfield/cli/build/utils/ios.d.ts
index d891ecec29c0f6..136fed4201dcb2 100644
--- a/packages/expo-brownfield/cli/build/utils/ios.d.ts
+++ b/packages/expo-brownfield/cli/build/utils/ios.d.ts
@@ -1,11 +1,16 @@
import { IosConfig } from './types';
export declare const cleanUpArtifacts: (config: IosConfig) => Promise;
export declare const buildFramework: (config: IosConfig) => Promise;
-export declare const copyHermesXcframework: (config: IosConfig) => Promise;
-export declare const createSwiftPackage: (config: IosConfig) => Promise;
-export declare const createXcframework: (config: IosConfig) => Promise;
+export declare const copyXCFrameworks: (config: IosConfig, dest: string) => Promise;
+export declare const createSwiftPackage: (config: IosConfig) => Promise;
+export declare const createXCframework: (config: IosConfig, at: string) => Promise;
export declare const findScheme: () => string | undefined;
export declare const findWorkspace: (dryRun: boolean) => string | undefined;
export declare const generatePackageMetadataFile: (config: IosConfig, packagePath: string) => Promise;
+export declare const getSupportedPlatforms: (config: IosConfig) => Promise;
+export declare const libraryProduct: (name: string, targets: string[]) => string;
+export declare const binaryTarget: (name: string) => string;
export declare const makeArtifactsDirectory: (config: IosConfig) => void;
export declare const printIosConfig: (config: IosConfig) => void;
+export declare const shipFrameworks: (config: IosConfig) => Promise;
+export declare const shipSwiftPackage: (config: IosConfig) => Promise;
diff --git a/packages/expo-brownfield/cli/build/utils/ios.js b/packages/expo-brownfield/cli/build/utils/ios.js
index 73cef7776e0ffd..f699caa0c62094 100644
--- a/packages/expo-brownfield/cli/build/utils/ios.js
+++ b/packages/expo-brownfield/cli/build/utils/ios.js
@@ -3,11 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
-exports.printIosConfig = exports.makeArtifactsDirectory = exports.generatePackageMetadataFile = exports.findWorkspace = exports.findScheme = exports.createXcframework = exports.createSwiftPackage = exports.copyHermesXcframework = exports.buildFramework = exports.cleanUpArtifacts = void 0;
+exports.shipSwiftPackage = exports.shipFrameworks = exports.printIosConfig = exports.makeArtifactsDirectory = exports.binaryTarget = exports.libraryProduct = exports.getSupportedPlatforms = exports.generatePackageMetadataFile = exports.findWorkspace = exports.findScheme = exports.createXCframework = exports.createSwiftPackage = exports.copyXCFrameworks = exports.buildFramework = exports.cleanUpArtifacts = void 0;
const chalk_1 = __importDefault(require("chalk"));
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const commands_1 = require("./commands");
+const constants_1 = require("./constants");
const error_1 = __importDefault(require("./error"));
const spinner_1 = require("./spinner");
const cleanUpArtifacts = async (config) => {
@@ -59,39 +60,43 @@ const buildFramework = async (config) => {
});
};
exports.buildFramework = buildFramework;
-const copyHermesXcframework = async (config) => {
- const destinationPath = config.output === 'frameworks'
- ? `${config.artifacts}/hermesvm.xcframework`
- : `${config.artifacts}/${config.output.packageName}/xcframeworks/hermesvm.xcframework`;
+const copyXCFrameworks = async (config, dest) => {
+ console.log('Copying XCFrameworks to:', dest);
if (config.dryRun) {
- console.log(`Copying hermes XCFramework from ${config.hermesFrameworkPath} to ${destinationPath}`);
return;
}
- const sourcePath = `./ios/${config.hermesFrameworkPath}`;
- if (!node_fs_1.default.existsSync(sourcePath)) {
- error_1.default.handle('ios-hermes-framework-not-found', sourcePath);
+ const xcframeworks = Object.values(constants_1.XCFramework);
+ for (const xcframework of xcframeworks) {
+ if (node_fs_1.default.existsSync(xcframework.path)) {
+ await (0, spinner_1.withSpinner)({
+ operation: async () => node_fs_1.default.promises.cp(xcframework.path, node_path_1.default.join(dest, `${xcframework.name}.xcframework`), {
+ force: true,
+ recursive: true,
+ }),
+ loaderMessage: `Copying ${xcframework.name} to the artifacts directory...`,
+ successMessage: `Copying ${xcframework.name} to the artifacts directory succeeded`,
+ errorMessage: `Copying ${xcframework.name} to the artifacts directory failed`,
+ verbose: config.verbose,
+ });
+ }
+ else if (xcframework.name === constants_1.XCFramework.Hermes.name) {
+ error_1.default.handle('ios-hermes-framework-not-found', xcframework.path);
+ }
+ else {
+ console.warn(`${xcframework.name} not found in source path: ${xcframework.path}. Assuming it's built from sources`);
+ }
}
- return (0, spinner_1.withSpinner)({
- operation: async () => node_fs_1.default.promises.cp(sourcePath, destinationPath, {
- force: true,
- recursive: true,
- }),
- loaderMessage: 'Copying hermesvm.xcframework to the artifacts directory...',
- successMessage: 'Copying hermesvm.xcframework to the artifacts directory succeeded',
- errorMessage: 'Copying hermesvm.xcframework to the artifacts directory failed',
- verbose: config.verbose,
- });
};
-exports.copyHermesXcframework = copyHermesXcframework;
+exports.copyXCFrameworks = copyXCFrameworks;
const createSwiftPackage = async (config) => {
if (config.dryRun && config.output !== 'frameworks') {
console.log(`Creating Swift package with name: ${config.output.packageName} at path: ${config.artifacts}`);
- return;
+ return '';
}
- return (0, spinner_1.withSpinner)({
+ return await (0, spinner_1.withSpinner)({
operation: async () => {
if (config.output === 'frameworks') {
- return;
+ return '';
}
const packagePath = node_path_1.default.join(config.artifacts, config.output.packageName);
await node_fs_1.default.promises.mkdir(packagePath, { recursive: true });
@@ -99,6 +104,7 @@ const createSwiftPackage = async (config) => {
const xcframeworksDir = node_path_1.default.join(packagePath, 'xcframeworks');
await node_fs_1.default.promises.mkdir(xcframeworksDir, { recursive: true });
await (0, exports.generatePackageMetadataFile)(config, packagePath);
+ return packagePath;
},
loaderMessage: 'Creating Swift package...',
successMessage: 'Creating Swift package succeeded',
@@ -107,10 +113,9 @@ const createSwiftPackage = async (config) => {
});
};
exports.createSwiftPackage = createSwiftPackage;
-const createXcframework = async (config) => {
- const output = config.output === 'frameworks'
- ? `${config.artifacts}/${config.scheme}.xcframework`
- : `${config.artifacts}/${config.output.packageName}/xcframeworks/${config.scheme}.xcframework`;
+const createXCframework = async (config, at) => {
+ const frameworkName = `${config.scheme}.xcframework`;
+ const outputPath = node_path_1.default.join(at, frameworkName);
const args = [
'-create-xcframework',
'-framework',
@@ -118,7 +123,7 @@ const createXcframework = async (config) => {
'-framework',
`${config.simulator}/${config.scheme}.framework`,
'-output',
- output,
+ outputPath,
];
if (config.dryRun) {
console.log(`xcodebuild ${args.join(' ')}`);
@@ -132,7 +137,7 @@ const createXcframework = async (config) => {
verbose: config.verbose,
});
};
-exports.createXcframework = createXcframework;
+exports.createXCframework = createXCframework;
const findScheme = () => {
try {
const iosPath = node_path_1.default.join(process.cwd(), 'ios');
@@ -187,37 +192,64 @@ const generatePackageMetadataFile = async (config, packagePath) => {
if (config.output === 'frameworks') {
return;
}
+ const prebuiltFrameworks = node_fs_1.default.existsSync(constants_1.XCFramework.React.path);
const xcframeworks = [
{ name: config.scheme, targets: [config.scheme] },
{ name: 'hermesvm', targets: ['hermesvm'] },
+ ...(prebuiltFrameworks ? [constants_1.XCFramework.React, constants_1.XCFramework.ReactDependencies] : []),
];
const contents = `// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "${config.output.packageName}",
- platforms: [.iOS(.v15)],
- products: [${xcframeworks
- .map((xcf) => `
- .library(
- name: "${xcf.name}",
- targets: ["${xcf.targets.join('", "')}"],
- ),
- `)
- .join('\n')}],
- targets: [${xcframeworks
- .map((xcf) => `
- .binaryTarget(
- name: "${xcf.name}",
- path: "xcframeworks/${xcf.name}.xcframework",
- ),
- `)
- .join('\n')}]
+ platforms: [${(await (0, exports.getSupportedPlatforms)(config)).join(',')}],
+ products: [
+${xcframeworks.map(({ name, targets }) => (0, exports.libraryProduct)(name, targets)).join('\n')}
+ ],
+ targets: [
+${xcframeworks.map(({ name }) => (0, exports.binaryTarget)(name)).join('\n')}
+ ],
);
`;
await node_fs_1.default.promises.writeFile(node_path_1.default.join(packagePath, 'Package.swift'), contents);
};
exports.generatePackageMetadataFile = generatePackageMetadataFile;
+const getSupportedPlatforms = async (config) => {
+ // Try to infer `IPHONEOS_DEPLOYMENT_TARGET` from the project
+ const args = ['-workspace', config.workspace, '-scheme', config.scheme, '-showBuildSettings'];
+ try {
+ const { stdout } = await (0, commands_1.runCommand)('xcodebuild', args, { verbose: false });
+ const regex = /^\s*IPHONEOS_DEPLOYMENT_TARGET = (.+)$/m;
+ const value = regex.exec(stdout)?.[1].trim();
+ if (value) {
+ return [`.iOS("${value}")`];
+ }
+ else {
+ throw new Error();
+ }
+ }
+ catch (error) {
+ console.warn('Failed to infer `IPHONEOS_DEPLOYMENT_TARGET` from the project, defaulting to iOS v15');
+ }
+ // If failed to infer default to iOS v15
+ return ['.iOS(.v15)'];
+};
+exports.getSupportedPlatforms = getSupportedPlatforms;
+const libraryProduct = (name, targets) => {
+ return ` .library(
+ name: "${name}",
+ targets: ["${targets.join('", "')}"],
+ ),`;
+};
+exports.libraryProduct = libraryProduct;
+const binaryTarget = (name) => {
+ return ` .binaryTarget(
+ name: "${name}",
+ path: "xcframeworks/${name}.xcframework",
+ ),`;
+};
+exports.binaryTarget = binaryTarget;
const makeArtifactsDirectory = (config) => {
try {
if (!node_fs_1.default.existsSync(config.artifacts)) {
@@ -244,3 +276,23 @@ const printIosConfig = (config) => {
console.log();
};
exports.printIosConfig = printIosConfig;
+const shipFrameworks = async (config) => {
+ // Create artifacts directory
+ await (0, exports.cleanUpArtifacts)(config);
+ (0, exports.makeArtifactsDirectory)(config);
+ // Copy/create XCFrameworks into the package
+ await (0, exports.createXCframework)(config, config.artifacts);
+ await (0, exports.copyXCFrameworks)(config, config.artifacts);
+};
+exports.shipFrameworks = shipFrameworks;
+const shipSwiftPackage = async (config) => {
+ // Create artifacts directory and swift package
+ await (0, exports.cleanUpArtifacts)(config);
+ (0, exports.makeArtifactsDirectory)(config);
+ const packagePath = await (0, exports.createSwiftPackage)(config);
+ const xcframeworksPath = node_path_1.default.join(packagePath, 'xcframeworks');
+ // Copy/create XCFrameworks into the package
+ await (0, exports.createXCframework)(config, xcframeworksPath);
+ await (0, exports.copyXCFrameworks)(config, xcframeworksPath);
+};
+exports.shipSwiftPackage = shipSwiftPackage;
diff --git a/packages/expo-brownfield/cli/build/utils/types.d.ts b/packages/expo-brownfield/cli/build/utils/types.d.ts
index 47bef6ed786ecb..2a0ffac7f3c7cd 100644
--- a/packages/expo-brownfield/cli/build/utils/types.d.ts
+++ b/packages/expo-brownfield/cli/build/utils/types.d.ts
@@ -34,7 +34,6 @@ export interface IosConfig extends CommonConfig {
buildConfiguration: BuildConfiguration;
derivedDataPath: string;
device: string;
- hermesFrameworkPath: string;
output: 'frameworks' | PackageConfiguration;
scheme: string;
simulator: string;
@@ -43,3 +42,8 @@ export interface IosConfig extends CommonConfig {
export interface TasksConfigAndroid extends CommonConfig {
library: string;
}
+export interface XCFrameworkSpec {
+ name: string;
+ path: string;
+ targets: string[];
+}
diff --git a/packages/expo-brownfield/cli/src/commands/build-ios.ts b/packages/expo-brownfield/cli/src/commands/build-ios.ts
index 239a4458b433ee..922a33c06abd5a 100644
--- a/packages/expo-brownfield/cli/src/commands/build-ios.ts
+++ b/packages/expo-brownfield/cli/src/commands/build-ios.ts
@@ -2,28 +2,27 @@ import type { Command } from 'commander';
import {
buildFramework,
- cleanUpArtifacts,
- createSwiftPackage,
- createXcframework,
- copyHermesXcframework,
- makeArtifactsDirectory,
printIosConfig,
resolveBuildConfigIos,
validatePrebuild,
+ shipSwiftPackage,
+ shipFrameworks,
} from '../utils';
const buildIos = async (command: Command) => {
await validatePrebuild('ios');
-
const config = resolveBuildConfigIos(command.opts());
printIosConfig(config);
- await cleanUpArtifacts(config);
- makeArtifactsDirectory(config);
await buildFramework(config);
- await createSwiftPackage(config);
- await createXcframework(config);
- await copyHermesXcframework(config);
+
+ if (config.output !== 'frameworks') {
+ // Ship frameworks as swift package
+ shipSwiftPackage(config);
+ } else {
+ // Ship frameworks as standalone XCFrameworks
+ shipFrameworks(config);
+ }
};
export default buildIos;
diff --git a/packages/expo-brownfield/cli/src/utils/config.ts b/packages/expo-brownfield/cli/src/utils/config.ts
index 8326dd522fcf8b..b9043bc0d5278e 100644
--- a/packages/expo-brownfield/cli/src/utils/config.ts
+++ b/packages/expo-brownfield/cli/src/utils/config.ts
@@ -57,8 +57,7 @@ export const resolveBuildConfigIos = (options: OptionValues): IosConfig => {
derivedDataPath,
device,
simulator,
- hermesFrameworkPath,
- scheme,
+ scheme: resolveScheme(options),
workspace: resolveWorkspace(options),
};
};
diff --git a/packages/expo-brownfield/cli/src/utils/constants.ts b/packages/expo-brownfield/cli/src/utils/constants.ts
new file mode 100644
index 00000000000000..75cceff177b5fa
--- /dev/null
+++ b/packages/expo-brownfield/cli/src/utils/constants.ts
@@ -0,0 +1,19 @@
+import type { XCFrameworkSpec } from './types';
+
+export const XCFramework: Record = {
+ Hermes: {
+ path: 'ios/Pods/hermes-engine/destroot/Library/Frameworks/universal/hermesvm.xcframework',
+ name: 'hermesvm',
+ targets: ['hermesvm'],
+ },
+ React: {
+ path: 'ios/Pods/React-Core-prebuilt/React.xcframework',
+ name: 'React',
+ targets: ['React'],
+ },
+ ReactDependencies: {
+ path: 'ios/Pods/ReactNativeDependencies/framework/packages/react-native/ReactNativeDependencies.xcframework',
+ name: 'ReactNativeDependencies',
+ targets: ['ReactNativeDependencies'],
+ },
+};
diff --git a/packages/expo-brownfield/cli/src/utils/index.ts b/packages/expo-brownfield/cli/src/utils/index.ts
index d47b08a1c809ce..608f8369ebd348 100644
--- a/packages/expo-brownfield/cli/src/utils/index.ts
+++ b/packages/expo-brownfield/cli/src/utils/index.ts
@@ -1,5 +1,6 @@
export * from './android';
export * from './commands';
+export * from './constants';
export * from './config';
export { default as CLIError } from './error';
export * from './ios';
diff --git a/packages/expo-brownfield/cli/src/utils/ios.ts b/packages/expo-brownfield/cli/src/utils/ios.ts
index 0ec38f74ac182b..66d7fe21faf663 100644
--- a/packages/expo-brownfield/cli/src/utils/ios.ts
+++ b/packages/expo-brownfield/cli/src/utils/ios.ts
@@ -3,9 +3,10 @@ import fs from 'node:fs';
import path from 'node:path';
import { runCommand } from './commands';
+import { XCFramework } from './constants';
import CLIError from './error';
import { withSpinner } from './spinner';
-import { IosConfig } from './types';
+import { IosConfig, XCFrameworkSpec } from './types';
export const cleanUpArtifacts = async (config: IosConfig) => {
if (config.dryRun) {
@@ -60,49 +61,49 @@ export const buildFramework = async (config: IosConfig) => {
});
};
-export const copyHermesXcframework = async (config: IosConfig) => {
- const destinationPath =
- config.output === 'frameworks'
- ? `${config.artifacts}/hermesvm.xcframework`
- : `${config.artifacts}/${config.output.packageName}/xcframeworks/hermesvm.xcframework`;
+export const copyXCFrameworks = async (config: IosConfig, dest: string) => {
+ console.log('Copying XCFrameworks to:', dest);
if (config.dryRun) {
- console.log(
- `Copying hermes XCFramework from ${config.hermesFrameworkPath} to ${destinationPath}`
- );
return;
}
- const sourcePath = `./ios/${config.hermesFrameworkPath}`;
- if (!fs.existsSync(sourcePath)) {
- CLIError.handle('ios-hermes-framework-not-found', sourcePath);
+ const xcframeworks = Object.values(XCFramework);
+ for (const xcframework of xcframeworks) {
+ if (fs.existsSync(xcframework.path)) {
+ await withSpinner({
+ operation: async () =>
+ fs.promises.cp(xcframework.path, path.join(dest, `${xcframework.name}.xcframework`), {
+ force: true,
+ recursive: true,
+ }),
+ loaderMessage: `Copying ${xcframework.name} to the artifacts directory...`,
+ successMessage: `Copying ${xcframework.name} to the artifacts directory succeeded`,
+ errorMessage: `Copying ${xcframework.name} to the artifacts directory failed`,
+ verbose: config.verbose,
+ });
+ } else if (xcframework.name === XCFramework.Hermes.name) {
+ CLIError.handle('ios-hermes-framework-not-found', xcframework.path);
+ } else {
+ console.warn(
+ `${xcframework.name} not found in source path: ${xcframework.path}. Assuming it's built from sources`
+ );
+ }
}
-
- return withSpinner({
- operation: async () =>
- fs.promises.cp(sourcePath, destinationPath, {
- force: true,
- recursive: true,
- }),
- loaderMessage: 'Copying hermesvm.xcframework to the artifacts directory...',
- successMessage: 'Copying hermesvm.xcframework to the artifacts directory succeeded',
- errorMessage: 'Copying hermesvm.xcframework to the artifacts directory failed',
- verbose: config.verbose,
- });
};
-export const createSwiftPackage = async (config: IosConfig) => {
+export const createSwiftPackage = async (config: IosConfig): Promise => {
if (config.dryRun && config.output !== 'frameworks') {
console.log(
`Creating Swift package with name: ${config.output.packageName} at path: ${config.artifacts}`
);
- return;
+ return '';
}
- return withSpinner({
+ return await withSpinner({
operation: async () => {
if (config.output === 'frameworks') {
- return;
+ return '';
}
const packagePath = path.join(config.artifacts, config.output.packageName);
@@ -113,6 +114,8 @@ export const createSwiftPackage = async (config: IosConfig) => {
await fs.promises.mkdir(xcframeworksDir, { recursive: true });
await generatePackageMetadataFile(config, packagePath);
+
+ return packagePath;
},
loaderMessage: 'Creating Swift package...',
successMessage: 'Creating Swift package succeeded',
@@ -121,11 +124,9 @@ export const createSwiftPackage = async (config: IosConfig) => {
});
};
-export const createXcframework = async (config: IosConfig) => {
- const output =
- config.output === 'frameworks'
- ? `${config.artifacts}/${config.scheme}.xcframework`
- : `${config.artifacts}/${config.output.packageName}/xcframeworks/${config.scheme}.xcframework`;
+export const createXCframework = async (config: IosConfig, at: string) => {
+ const frameworkName = `${config.scheme}.xcframework`;
+ const outputPath = path.join(at, frameworkName);
const args = [
'-create-xcframework',
@@ -134,7 +135,7 @@ export const createXcframework = async (config: IosConfig) => {
'-framework',
`${config.simulator}/${config.scheme}.framework`,
'-output',
- output,
+ outputPath,
];
if (config.dryRun) {
@@ -212,9 +213,11 @@ export const generatePackageMetadataFile = async (config: IosConfig, packagePath
return;
}
+ const prebuiltFrameworks = fs.existsSync(XCFramework.React.path);
const xcframeworks = [
{ name: config.scheme, targets: [config.scheme] },
{ name: 'hermesvm', targets: ['hermesvm'] },
+ ...(prebuiltFrameworks ? [XCFramework.React, XCFramework.ReactDependencies] : []),
];
const contents = `// swift-tools-version:5.9
@@ -222,33 +225,56 @@ import PackageDescription
let package = Package(
name: "${config.output.packageName}",
- platforms: [.iOS(.v15)],
- products: [${xcframeworks
- .map(
- (xcf) => `
- .library(
- name: "${xcf.name}",
- targets: ["${xcf.targets.join('", "')}"],
- ),
- `
- )
- .join('\n')}],
- targets: [${xcframeworks
- .map(
- (xcf) => `
- .binaryTarget(
- name: "${xcf.name}",
- path: "xcframeworks/${xcf.name}.xcframework",
- ),
- `
- )
- .join('\n')}]
+ platforms: [${(await getSupportedPlatforms(config)).join(',')}],
+ products: [
+${xcframeworks.map(({ name, targets }) => libraryProduct(name, targets)).join('\n')}
+ ],
+ targets: [
+${xcframeworks.map(({ name }) => binaryTarget(name)).join('\n')}
+ ],
);
`;
await fs.promises.writeFile(path.join(packagePath, 'Package.swift'), contents);
};
+export const getSupportedPlatforms = async (config: IosConfig): Promise => {
+ // Try to infer `IPHONEOS_DEPLOYMENT_TARGET` from the project
+ const args = ['-workspace', config.workspace, '-scheme', config.scheme, '-showBuildSettings'];
+
+ try {
+ const { stdout } = await runCommand('xcodebuild', args, { verbose: false });
+ const regex = /^\s*IPHONEOS_DEPLOYMENT_TARGET = (.+)$/m;
+ const value = regex.exec(stdout)?.[1].trim();
+ if (value) {
+ return [`.iOS("${value}")`];
+ } else {
+ throw new Error();
+ }
+ } catch (error) {
+ console.warn(
+ 'Failed to infer `IPHONEOS_DEPLOYMENT_TARGET` from the project, defaulting to iOS v15'
+ );
+ }
+
+ // If failed to infer default to iOS v15
+ return ['.iOS(.v15)'];
+};
+
+export const libraryProduct = (name: string, targets: string[]) => {
+ return ` .library(
+ name: "${name}",
+ targets: ["${targets.join('", "')}"],
+ ),`;
+};
+
+export const binaryTarget = (name: string) => {
+ return ` .binaryTarget(
+ name: "${name}",
+ path: "xcframeworks/${name}.xcframework",
+ ),`;
+};
+
export const makeArtifactsDirectory = (config: IosConfig) => {
try {
if (!fs.existsSync(config.artifacts)) {
@@ -275,3 +301,25 @@ export const printIosConfig = (config: IosConfig) => {
console.log();
};
+
+export const shipFrameworks = async (config: IosConfig) => {
+ // Create artifacts directory
+ await cleanUpArtifacts(config);
+ makeArtifactsDirectory(config);
+
+ // Copy/create XCFrameworks into the package
+ await createXCframework(config, config.artifacts);
+ await copyXCFrameworks(config, config.artifacts);
+};
+
+export const shipSwiftPackage = async (config: IosConfig) => {
+ // Create artifacts directory and swift package
+ await cleanUpArtifacts(config);
+ makeArtifactsDirectory(config);
+ const packagePath = await createSwiftPackage(config);
+ const xcframeworksPath = path.join(packagePath, 'xcframeworks');
+
+ // Copy/create XCFrameworks into the package
+ await createXCframework(config, xcframeworksPath);
+ await copyXCFrameworks(config, xcframeworksPath);
+};
diff --git a/packages/expo-brownfield/cli/src/utils/types.ts b/packages/expo-brownfield/cli/src/utils/types.ts
index 5a3f9565c6c52c..b9c20f19e58a7e 100644
--- a/packages/expo-brownfield/cli/src/utils/types.ts
+++ b/packages/expo-brownfield/cli/src/utils/types.ts
@@ -43,7 +43,6 @@ export interface IosConfig extends CommonConfig {
buildConfiguration: BuildConfiguration;
derivedDataPath: string;
device: string;
- hermesFrameworkPath: string;
output: 'frameworks' | PackageConfiguration;
scheme: string;
simulator: string;
@@ -53,3 +52,9 @@ export interface IosConfig extends CommonConfig {
export interface TasksConfigAndroid extends CommonConfig {
library: string;
}
+
+export interface XCFrameworkSpec {
+ name: string;
+ path: string;
+ targets: string[];
+}
diff --git a/packages/expo-brownfield/e2e/cli/__tests__/build-ios.test.ts b/packages/expo-brownfield/e2e/cli/__tests__/build-ios.test.ts
index 05a196f7ddab00..f290f3b8743805 100644
--- a/packages/expo-brownfield/e2e/cli/__tests__/build-ios.test.ts
+++ b/packages/expo-brownfield/e2e/cli/__tests__/build-ios.test.ts
@@ -156,7 +156,7 @@ describe('build:ios command', () => {
BUILD_IOS.ARTIFACT_CLEANUP,
...BUILD_IOS.BUILD_COMMAND(TEMP_DIR_PREBUILD, PREBUILD_WORKSPACE_NAME, 'Release'),
...BUILD_IOS.PACKAGE_COMMAND(TEMP_DIR_PREBUILD, PREBUILD_WORKSPACE_NAME, 'Release'),
- BUILD_IOS.HERMES_COPYING,
+ BUILD_IOS.HERMES_COPYING(TEMP_DIR_PREBUILD),
],
});
});
diff --git a/packages/expo-brownfield/e2e/plugin/__tests__/plugin-ios.test.ts b/packages/expo-brownfield/e2e/plugin/__tests__/plugin-ios.test.ts
index 3048065e63dff9..0169ab7a40e497 100644
--- a/packages/expo-brownfield/e2e/plugin/__tests__/plugin-ios.test.ts
+++ b/packages/expo-brownfield/e2e/plugin/__tests__/plugin-ios.test.ts
@@ -58,7 +58,6 @@ describe('plugin for ios', () => {
it('modifies the podfile properties', async () => {
validatePodfileProperties(TEMP_DIR, {
'ios.useFrameworks': 'static',
- 'ios.brownfieldTargetName': 'testapppluginiosbrownfield',
});
});
diff --git a/packages/expo-brownfield/e2e/scripts/add_xcframeworks.rb b/packages/expo-brownfield/e2e/scripts/add_xcframeworks.rb
index cc870b78d4e1c2..7ddb4aa53c330f 100644
--- a/packages/expo-brownfield/e2e/scripts/add_xcframeworks.rb
+++ b/packages/expo-brownfield/e2e/scripts/add_xcframeworks.rb
@@ -1,25 +1,27 @@
require 'xcodeproj'
require 'pathname'
+require 'json'
workspace_root = ENV['GITHUB_WORKSPACE']
-frameworks_path = File.join(workspace_root, 'artifacts')
-
-# Ensure all needed frameworks exist
-frameworks = ["expoappbrownfield.xcframework", "hermesvm.xcframework"]
-for framework in frameworks do
- framework_path = File.join(frameworks_path, framework)
- unless File.exist?(framework_path)
- puts "Error: #{framework} XCFramework not found at #{framework_path}"
- exit 1
- end
+package_path = File.join(workspace_root, 'artifacts', 'BrownfieldPackage')
+
+# Ensure package exists
+unless File.exist?(File.join(package_path, 'Package.swift'))
+ puts "Error: BrownfieldPackage not found at #{package_path}"
+ exit 1
end
+# Read package products from Package.swift
+package_info = JSON.parse(`cd #{package_path} && swift package dump-package`)
+product_names = package_info['products'].map { |p| p['name'] }
+puts "Found package products: #{product_names.join(', ')}"
+
# Modify SwiftUI project
swiftui_target_name = "BrownfieldIntegratedTester"
swiftui_project_path = File.join(
- workspace_root,
- 'apps',
- 'brownfield-tester',
+ workspace_root,
+ 'apps',
+ 'brownfield-tester',
'ios-integrated',
"#{swiftui_target_name}.xcodeproj"
)
@@ -28,29 +30,65 @@
swiftui_target = swiftui_project.targets.find { |t| t.name == swiftui_target_name }
raise "Target #{swiftui_target_name} not found" unless swiftui_target
-swiftui_project_dir = Pathname.new(File.dirname(swiftui_project_path))
-swiftui_frameworks_group = swiftui_project.main_group['Frameworks'] || swiftui_project.new_group('Frameworks')
+# ── Cleanup ────────────────────────────────────────────────────────────────────
+
+# Remove build files that reference package products
+swiftui_target.frameworks_build_phase.files.select { |f|
+ f.respond_to?(:product_ref) && !f.product_ref.nil?
+}.each(&:remove_from_project)
-embed_phase = swiftui_target.build_phases.find do |b|
- b.class == Xcodeproj::Project::Object::PBXCopyFilesBuildPhase && b.dst_subfolder_spec == "10"
+# Remove all files from frameworks build phase (XCFrameworks)
+swiftui_target.frameworks_build_phase.files.each(&:remove_from_project)
+
+# Remove embed frameworks phase
+embed_phase = swiftui_target.build_phases.find do |b|
+ b.class == Xcodeproj::Project::Object::PBXCopyFilesBuildPhase && b.dst_subfolder_spec == "10"
+end
+if embed_phase
+ embed_phase.files.each(&:remove_from_project)
+ embed_phase.remove_from_project
end
-if embed_phase.nil?
- embed_phase = swiftui_target.new_copy_files_build_phase("Embed Frameworks")
- embed_phase.dst_subfolder_spec = "10"
+
+# Remove frameworks group
+frameworks_group = swiftui_project.main_group['Frameworks']
+if frameworks_group
+ frameworks_group.files.each(&:remove_from_project)
+ frameworks_group.remove_from_project
end
-frameworks.each do |framework|
- framework_path = Pathname.new(File.join(frameworks_path, framework))
- relative_framework_path = framework_path.relative_path_from(swiftui_project_dir).to_s
- framework_ref = swiftui_frameworks_group.files.find { |f| f.path == relative_framework_path } ||
- swiftui_frameworks_group.new_file(relative_framework_path)
+# Remove existing package product dependencies from target
+swiftui_target.package_product_dependencies.each(&:remove_from_project)
+
+# Remove existing package references from project (both local and remote)
+swiftui_project.root_object.package_references.each(&:remove_from_project)
+
+puts "Cleaned up existing frameworks and packages"
+
+# ── Add Local Swift Package ────────────────────────────────────────────────────
+
+swiftui_project_dir = Pathname.new(File.dirname(swiftui_project_path))
+relative_package_path = Pathname.new(package_path).relative_path_from(swiftui_project_dir).to_s
+
+# Add local package reference to project
+package_ref = swiftui_project.new(Xcodeproj::Project::Object::XCLocalSwiftPackageReference)
+package_ref.relative_path = relative_package_path
+swiftui_project.root_object.package_references << package_ref
+
+puts "Added local package reference: #{relative_package_path}"
- swiftui_target.frameworks_build_phase.add_file_reference(framework_ref)
+# Add each product as a dependency on the target
+product_names.each do |product_name|
+ package_product = swiftui_project.new(Xcodeproj::Project::Object::XCSwiftPackageProductDependency)
+ package_product.package = package_ref
+ package_product.product_name = product_name
+ swiftui_target.package_product_dependencies << package_product
- build_file = embed_phase.add_file_reference(framework_ref)
- build_file.settings = { 'ATTRIBUTES' => ['CodeSignOnCopy', 'RemoveHeadersOnCopy'] }
+ build_file = swiftui_project.new(Xcodeproj::Project::Object::PBXBuildFile)
+ build_file.product_ref = package_product
+ swiftui_target.frameworks_build_phase.files << build_file
- puts "Added #{relative_framework_path} to #{swiftui_target_name}"
+ puts "Added product '#{product_name}' to #{swiftui_target_name}"
end
swiftui_project.save
+puts "Project saved successfully"
diff --git a/packages/expo-brownfield/e2e/utils/output.ts b/packages/expo-brownfield/e2e/utils/output.ts
index c64a424684df0e..81769d99bcc2cb 100644
--- a/packages/expo-brownfield/e2e/utils/output.ts
+++ b/packages/expo-brownfield/e2e/utils/output.ts
@@ -54,7 +54,7 @@ export const BUILD_IOS = {
`- Verbose: false`,
`- Artifacts path: ${projectRoot}/artifacts`,
],
- HERMES_COPYING: `Copying hermes XCFramework from Pods/hermes-engine/destroot/Library/Frameworks/universal/hermesvm.xcframework to`,
+ HERMES_COPYING: (projectRoot: string) => `Copying XCFrameworks to: ${projectRoot}/artifacts`,
PACKAGE_COMMAND: (projectRoot: string, workspace: string, configuration: 'Debug' | 'Release') => [
`xcodebuild`,
`-create-xcframework`,
diff --git a/packages/expo-brownfield/plugin/build/ios/plugins/withBuildPropertiesPlugin.d.ts b/packages/expo-brownfield/plugin/build/ios/plugins/withBuildPropertiesPlugin.d.ts
index 8d381a7494bf4f..c279d65b5d4802 100644
--- a/packages/expo-brownfield/plugin/build/ios/plugins/withBuildPropertiesPlugin.d.ts
+++ b/packages/expo-brownfield/plugin/build/ios/plugins/withBuildPropertiesPlugin.d.ts
@@ -1,3 +1,4 @@
import type { ConfigPlugin } from 'expo/config-plugins';
-declare const withBuildPropertiesPlugin: ConfigPlugin;
+import type { PluginConfig } from '../types';
+declare const withBuildPropertiesPlugin: ConfigPlugin;
export default withBuildPropertiesPlugin;
diff --git a/packages/expo-brownfield/plugin/build/ios/plugins/withBuildPropertiesPlugin.js b/packages/expo-brownfield/plugin/build/ios/plugins/withBuildPropertiesPlugin.js
index f5f7b4016b9dfb..8d9b18d5af5e90 100644
--- a/packages/expo-brownfield/plugin/build/ios/plugins/withBuildPropertiesPlugin.js
+++ b/packages/expo-brownfield/plugin/build/ios/plugins/withBuildPropertiesPlugin.js
@@ -4,9 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
};
Object.defineProperty(exports, "__esModule", { value: true });
const expo_build_properties_1 = __importDefault(require("expo-build-properties"));
-const withBuildPropertiesPlugin = (config) => {
+const withBuildPropertiesPlugin = (config, pluginConfig) => {
return (0, expo_build_properties_1.default)(config, {
- ios: { buildReactNativeFromSource: true },
+ ios: { buildReactNativeFromSource: !pluginConfig.usePrebuiltReactNative },
});
};
exports.default = withBuildPropertiesPlugin;
diff --git a/packages/expo-brownfield/plugin/build/ios/plugins/withPodfilePlugin.js b/packages/expo-brownfield/plugin/build/ios/plugins/withPodfilePlugin.js
index 83a802d866ba6b..b40c48a727f2bd 100644
--- a/packages/expo-brownfield/plugin/build/ios/plugins/withPodfilePlugin.js
+++ b/packages/expo-brownfield/plugin/build/ios/plugins/withPodfilePlugin.js
@@ -5,6 +5,9 @@ const utils_1 = require("../utils");
const withPodfilePlugin = (config, pluginConfig) => {
return (0, config_plugins_1.withPodfile)(config, (config) => {
config.modResults.contents = (0, utils_1.addNewPodsTarget)(config.modResults.contents, pluginConfig.targetName);
+ if (pluginConfig.usePrebuiltReactNative) {
+ config.modResults.contents = (0, utils_1.addPrebuiltSettings)(config.modResults.contents);
+ }
return config;
});
};
diff --git a/packages/expo-brownfield/plugin/build/ios/plugins/withPodfilePropertiesPlugin.js b/packages/expo-brownfield/plugin/build/ios/plugins/withPodfilePropertiesPlugin.js
index 66244d892e2ab1..e9270ab0ef38f3 100644
--- a/packages/expo-brownfield/plugin/build/ios/plugins/withPodfilePropertiesPlugin.js
+++ b/packages/expo-brownfield/plugin/build/ios/plugins/withPodfilePropertiesPlugin.js
@@ -3,8 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
const config_plugins_1 = require("expo/config-plugins");
const withPodfilePropertiesPlugin = (config, pluginConfig) => {
return (0, config_plugins_1.withPodfileProperties)(config, (config) => {
- config.modResults['ios.useFrameworks'] = 'static';
- config.modResults['ios.brownfieldTargetName'] = pluginConfig.targetName;
+ if (!pluginConfig.usePrebuiltReactNative) {
+ config.modResults['ios.useFrameworks'] = 'static';
+ }
return config;
});
};
diff --git a/packages/expo-brownfield/plugin/build/ios/types.d.ts b/packages/expo-brownfield/plugin/build/ios/types.d.ts
index eda58957a4b9fc..7e22fe588ef4f6 100644
--- a/packages/expo-brownfield/plugin/build/ios/types.d.ts
+++ b/packages/expo-brownfield/plugin/build/ios/types.d.ts
@@ -1,6 +1,7 @@
export interface PluginConfig {
bundleIdentifier: string;
targetName: string;
+ usePrebuiltReactNative: boolean;
}
export type IOSPluginProps = Partial;
export type PluginProps = IOSPluginProps | undefined;
diff --git a/packages/expo-brownfield/plugin/build/ios/utils/podfile.d.ts b/packages/expo-brownfield/plugin/build/ios/utils/podfile.d.ts
index 382abbed964f6d..65e3e5bcbd9c50 100644
--- a/packages/expo-brownfield/plugin/build/ios/utils/podfile.d.ts
+++ b/packages/expo-brownfield/plugin/build/ios/utils/podfile.d.ts
@@ -1 +1,2 @@
export declare const addNewPodsTarget: (podfile: string, targetName: string) => string;
+export declare const addPrebuiltSettings: (podfile: string) => string;
diff --git a/packages/expo-brownfield/plugin/build/ios/utils/podfile.js b/packages/expo-brownfield/plugin/build/ios/utils/podfile.js
index 0ceedf84fde7df..922abc865aaea0 100644
--- a/packages/expo-brownfield/plugin/build/ios/utils/podfile.js
+++ b/packages/expo-brownfield/plugin/build/ios/utils/podfile.js
@@ -1,9 +1,24 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
-exports.addNewPodsTarget = void 0;
+exports.addPrebuiltSettings = exports.addNewPodsTarget = void 0;
const getTargetNameLines = (targetName) => {
return [` target '${targetName}' do`, ' inherit! :complete', ' end'];
};
+/**
+ * Ensure that we disable SWIFT_VERIFY_EMITTED_MODULE_INTERFACE option
+ * and include -no-verify-emitted-module-interface in OTHER_SWIFT_FLAGS
+ * for all targets in the pods project in order for consuming prebuilt RN
+ * frameworks to work
+ */
+const getPrebuiltSettingsLines = () => {
+ return ` installer.pods_project.targets.each do |t|
+ t.build_configurations.each do |config|
+ config.build_settings['SWIFT_VERIFY_EMITTED_MODULE_INTERFACE'] = 'NO'
+ flags = config.build_settings['OTHER_SWIFT_FLAGS'] || '$(inherited)'
+ config.build_settings['OTHER_SWIFT_FLAGS'] = "#{flags} -no-verify-emitted-module-interface"
+ end
+ end`.split('\n');
+};
const addNewPodsTarget = (podfile, targetName) => {
const targetLines = getTargetNameLines(targetName);
let podFileLines = podfile.split('\n');
@@ -21,3 +36,21 @@ const addNewPodsTarget = (podfile, targetName) => {
return podFileLines.join('\n');
};
exports.addNewPodsTarget = addNewPodsTarget;
+const addPrebuiltSettings = (podfile) => {
+ const prebuiltSettingsLines = getPrebuiltSettingsLines();
+ let podFileLines = podfile.split('\n');
+ if (podFileLines.find((line) => line.includes(prebuiltSettingsLines[4].trim()))) {
+ console.info('Prebuilt settings are already added. Skipping...');
+ return podfile;
+ }
+ const postInstallIndex = podFileLines.findIndex((line) => line.includes('post_install do |installer|'));
+ const insertBefore = podFileLines.findIndex((line, index) => line.includes('end') && index > postInstallIndex);
+ podFileLines = [
+ ...podFileLines.slice(0, insertBefore),
+ '', // new line for nicer output
+ ...prebuiltSettingsLines,
+ ...podFileLines.slice(insertBefore),
+ ];
+ return podFileLines.join('\n');
+};
+exports.addPrebuiltSettings = addPrebuiltSettings;
diff --git a/packages/expo-brownfield/plugin/build/ios/utils/props.js b/packages/expo-brownfield/plugin/build/ios/utils/props.js
index 9e317843cd2bf6..2bb24d81d079b7 100644
--- a/packages/expo-brownfield/plugin/build/ios/utils/props.js
+++ b/packages/expo-brownfield/plugin/build/ios/utils/props.js
@@ -6,6 +6,7 @@ const getPluginConfig = (props, config) => {
return {
bundleIdentifier: getBundleIdentifier(props, config, targetName),
targetName,
+ usePrebuiltReactNative: props?.usePrebuiltReactNative ?? false,
};
};
exports.getPluginConfig = getPluginConfig;
diff --git a/packages/expo-brownfield/plugin/build/ios/withIosPlugin.js b/packages/expo-brownfield/plugin/build/ios/withIosPlugin.js
index 55d6d769974281..ecb10841566175 100644
--- a/packages/expo-brownfield/plugin/build/ios/withIosPlugin.js
+++ b/packages/expo-brownfield/plugin/build/ios/withIosPlugin.js
@@ -7,7 +7,7 @@ const withIosPlugin = (config, props) => {
config = (0, plugins_1.withXcodeProjectPlugin)(config, pluginConfig);
config = (0, plugins_1.withPodfilePlugin)(config, pluginConfig);
config = (0, plugins_1.withPodfilePropertiesPlugin)(config, pluginConfig);
- config = (0, plugins_1.withBuildPropertiesPlugin)(config);
+ config = (0, plugins_1.withBuildPropertiesPlugin)(config, pluginConfig);
return config;
};
exports.default = withIosPlugin;
diff --git a/packages/expo-brownfield/plugin/src/ios/plugins/withBuildPropertiesPlugin.ts b/packages/expo-brownfield/plugin/src/ios/plugins/withBuildPropertiesPlugin.ts
index da2bb581472028..8852db8ea290ed 100644
--- a/packages/expo-brownfield/plugin/src/ios/plugins/withBuildPropertiesPlugin.ts
+++ b/packages/expo-brownfield/plugin/src/ios/plugins/withBuildPropertiesPlugin.ts
@@ -1,9 +1,11 @@
import type { ConfigPlugin } from 'expo/config-plugins';
import withBuildProperties from 'expo-build-properties';
-const withBuildPropertiesPlugin: ConfigPlugin = (config) => {
+import type { PluginConfig } from '../types';
+
+const withBuildPropertiesPlugin: ConfigPlugin = (config, pluginConfig) => {
return withBuildProperties(config, {
- ios: { buildReactNativeFromSource: true },
+ ios: { buildReactNativeFromSource: !pluginConfig.usePrebuiltReactNative },
});
};
diff --git a/packages/expo-brownfield/plugin/src/ios/plugins/withPodfilePlugin.ts b/packages/expo-brownfield/plugin/src/ios/plugins/withPodfilePlugin.ts
index 36e97d90c504fa..a06c37a6dccaca 100644
--- a/packages/expo-brownfield/plugin/src/ios/plugins/withPodfilePlugin.ts
+++ b/packages/expo-brownfield/plugin/src/ios/plugins/withPodfilePlugin.ts
@@ -1,7 +1,7 @@
import { type ConfigPlugin, withPodfile } from 'expo/config-plugins';
import type { PluginConfig } from '../types';
-import { addNewPodsTarget } from '../utils';
+import { addNewPodsTarget, addPrebuiltSettings } from '../utils';
const withPodfilePlugin: ConfigPlugin = (config, pluginConfig) => {
return withPodfile(config, (config) => {
@@ -9,6 +9,9 @@ const withPodfilePlugin: ConfigPlugin = (config, pluginConfig) =>
config.modResults.contents,
pluginConfig.targetName
);
+ if (pluginConfig.usePrebuiltReactNative) {
+ config.modResults.contents = addPrebuiltSettings(config.modResults.contents);
+ }
return config;
});
};
diff --git a/packages/expo-brownfield/plugin/src/ios/plugins/withPodfilePropertiesPlugin.ts b/packages/expo-brownfield/plugin/src/ios/plugins/withPodfilePropertiesPlugin.ts
index 0d7e4e1e82c67b..a088e8b1b2baf2 100644
--- a/packages/expo-brownfield/plugin/src/ios/plugins/withPodfilePropertiesPlugin.ts
+++ b/packages/expo-brownfield/plugin/src/ios/plugins/withPodfilePropertiesPlugin.ts
@@ -4,8 +4,9 @@ import type { PluginConfig } from '../types';
const withPodfilePropertiesPlugin: ConfigPlugin = (config, pluginConfig) => {
return withPodfileProperties(config, (config) => {
- config.modResults['ios.useFrameworks'] = 'static';
- config.modResults['ios.brownfieldTargetName'] = pluginConfig.targetName;
+ if (!pluginConfig.usePrebuiltReactNative) {
+ config.modResults['ios.useFrameworks'] = 'static';
+ }
return config;
});
};
diff --git a/packages/expo-brownfield/plugin/src/ios/types.ts b/packages/expo-brownfield/plugin/src/ios/types.ts
index afe1b407fd168a..ded6c82de24ecd 100644
--- a/packages/expo-brownfield/plugin/src/ios/types.ts
+++ b/packages/expo-brownfield/plugin/src/ios/types.ts
@@ -1,6 +1,7 @@
export interface PluginConfig {
bundleIdentifier: string;
targetName: string;
+ usePrebuiltReactNative: boolean;
}
export type IOSPluginProps = Partial;
diff --git a/packages/expo-brownfield/plugin/src/ios/utils/podfile.ts b/packages/expo-brownfield/plugin/src/ios/utils/podfile.ts
index 67a0c22b5f52db..e5d02d8d8782ba 100644
--- a/packages/expo-brownfield/plugin/src/ios/utils/podfile.ts
+++ b/packages/expo-brownfield/plugin/src/ios/utils/podfile.ts
@@ -2,6 +2,22 @@ const getTargetNameLines = (targetName: string): string[] => {
return [` target '${targetName}' do`, ' inherit! :complete', ' end'];
};
+/**
+ * Ensure that we disable SWIFT_VERIFY_EMITTED_MODULE_INTERFACE option
+ * and include -no-verify-emitted-module-interface in OTHER_SWIFT_FLAGS
+ * for all targets in the pods project in order for consuming prebuilt RN
+ * frameworks to work
+ */
+const getPrebuiltSettingsLines = (): string[] => {
+ return ` installer.pods_project.targets.each do |t|
+ t.build_configurations.each do |config|
+ config.build_settings['SWIFT_VERIFY_EMITTED_MODULE_INTERFACE'] = 'NO'
+ flags = config.build_settings['OTHER_SWIFT_FLAGS'] || '$(inherited)'
+ config.build_settings['OTHER_SWIFT_FLAGS'] = "#{flags} -no-verify-emitted-module-interface"
+ end
+ end`.split('\n');
+};
+
export const addNewPodsTarget = (podfile: string, targetName: string): string => {
const targetLines = getTargetNameLines(targetName);
let podFileLines = podfile.split('\n');
@@ -20,3 +36,27 @@ export const addNewPodsTarget = (podfile: string, targetName: string): string =>
return podFileLines.join('\n');
};
+
+export const addPrebuiltSettings = (podfile: string): string => {
+ const prebuiltSettingsLines = getPrebuiltSettingsLines();
+ let podFileLines = podfile.split('\n');
+ if (podFileLines.find((line) => line.includes(prebuiltSettingsLines[4].trim()))) {
+ console.info('Prebuilt settings are already added. Skipping...');
+ return podfile;
+ }
+
+ const postInstallIndex = podFileLines.findIndex((line) =>
+ line.includes('post_install do |installer|')
+ );
+ const insertBefore = podFileLines.findIndex(
+ (line, index) => line.includes('end') && index > postInstallIndex
+ );
+ podFileLines = [
+ ...podFileLines.slice(0, insertBefore),
+ '', // new line for nicer output
+ ...prebuiltSettingsLines,
+ ...podFileLines.slice(insertBefore),
+ ];
+
+ return podFileLines.join('\n');
+};
diff --git a/packages/expo-brownfield/plugin/src/ios/utils/props.ts b/packages/expo-brownfield/plugin/src/ios/utils/props.ts
index cf5b8b967c5e4e..a3456028f1ceea 100644
--- a/packages/expo-brownfield/plugin/src/ios/utils/props.ts
+++ b/packages/expo-brownfield/plugin/src/ios/utils/props.ts
@@ -8,6 +8,7 @@ export const getPluginConfig = (props: PluginProps, config: ExpoConfig): PluginC
return {
bundleIdentifier: getBundleIdentifier(props, config, targetName),
targetName,
+ usePrebuiltReactNative: props?.usePrebuiltReactNative ?? false,
};
};
diff --git a/packages/expo-brownfield/plugin/src/ios/withIosPlugin.ts b/packages/expo-brownfield/plugin/src/ios/withIosPlugin.ts
index a14c1b46f06090..c9b526fe071aa9 100644
--- a/packages/expo-brownfield/plugin/src/ios/withIosPlugin.ts
+++ b/packages/expo-brownfield/plugin/src/ios/withIosPlugin.ts
@@ -15,7 +15,7 @@ const withIosPlugin: ConfigPlugin = (config, props) => {
config = withXcodeProjectPlugin(config, pluginConfig);
config = withPodfilePlugin(config, pluginConfig);
config = withPodfilePropertiesPlugin(config, pluginConfig);
- config = withBuildPropertiesPlugin(config);
+ config = withBuildPropertiesPlugin(config, pluginConfig);
return config;
};
diff --git a/packages/expo-video/CHANGELOG.md b/packages/expo-video/CHANGELOG.md
index 599e1c29d9f4fc..5530b7c54eca0e 100644
--- a/packages/expo-video/CHANGELOG.md
+++ b/packages/expo-video/CHANGELOG.md
@@ -254,6 +254,7 @@ _This version does not introduce any user-facing changes._
- [iOS] Fix player reporting status `readyToPlay` while a source is being loaded asynchronously. ([#37180](https://github.com/expo/expo/pull/37180) by [@behenate](https://github.com/behenate))
- [iOS] Fix player going into `loading` status for a single frame when unpausing with a full buffer. ([#37181](https://github.com/expo/expo/pull/37181) by [@behenate](https://github.com/behenate))
- [iOS] Fix player getting stuck in `loading` state for null sources. ([#37183](https://github.com/expo/expo/pull/37183) by [@behenate](https://github.com/behenate))
+- [iOS] Fix player not setting default audio track for some HLS streams. ([#37395](https://github.com/expo/expo/pull/37395) by [@vitorclelis96](https://github.com/vitorclelis96))
## 2.1.9 — 2025-05-08
diff --git a/packages/expo-video/ios/VideoPlayerAudioTracks.swift b/packages/expo-video/ios/VideoPlayerAudioTracks.swift
index 461e3cb3336a00..9f2c63cc4b79cd 100644
--- a/packages/expo-video/ios/VideoPlayerAudioTracks.swift
+++ b/packages/expo-video/ios/VideoPlayerAudioTracks.swift
@@ -29,6 +29,8 @@ class VideoPlayerAudioTracks {
availableAudioTracks = []
}
+ await Self.selectDefaultAudioTrackIfNeeded(for: playerItem)
+
return AudioTracksChangedEventPayload(
availableAudioTracks: availableAudioTracks,
oldAvailableAudioTracks: oldAvailableAudioTracks
@@ -75,6 +77,24 @@ class VideoPlayerAudioTracks {
return availableAudioTracks
}
+ private static func selectDefaultAudioTrackIfNeeded(for playerItem: AVPlayerItem?) async {
+ guard let playerItem,
+ let audioGroup = try? await playerItem.asset.loadMediaSelectionGroup(for: .audible) else {
+ return
+ }
+
+ let currentSelection = await playerItem.currentMediaSelection
+ guard currentSelection.selectedMediaOption(in: audioGroup) == nil else {
+ return
+ }
+
+ if let audioOption = audioGroup.defaultOption ?? audioGroup.options.first {
+ await MainActor.run {
+ playerItem.select(audioOption, in: audioGroup)
+ }
+ }
+ }
+
static func findCurrentAudioTrack(for playerItem: AVPlayerItem?) async -> AudioTrack? {
guard
let currentItem = playerItem,