From b3af0c659f7abd752388b51848c1db0bf6a7fe69 Mon Sep 17 00:00:00 2001 From: Patryk Mleczek <67064618+pmleczek@users.noreply.github.com> Date: Thu, 19 Feb 2026 17:59:41 +0100 Subject: [PATCH 1/5] [brownfield][ios] cleanup ios test app (#43266) # Why - We did similar cleanup for Android in https://github.com/expo/expo/pull/43097 - The View with RN app was too cluttered and hard to read # How Extracted all brownfield testing code to a separate `ObservableObject` # Test Plan Verified that the test app works as previously and communication API works # Checklist - [X] 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) - [X] This diff will work correctly for `npx expo prebuild` & EAS Build (eg: updated a module plugin). --- .../project.pbxproj | 23 ++++--- .../BrownfieldIntegratedTesterApp.swift | 2 +- .../BrownfieldTester.swift | 67 +++++++++++++++++++ .../ContentView.swift | 24 ++++--- 4 files changed, 97 insertions(+), 19 deletions(-) create mode 100644 apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/BrownfieldTester.swift diff --git a/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester.xcodeproj/project.pbxproj b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester.xcodeproj/project.pbxproj index 6c2096a8d6a829..27817d923a46ca 100644 --- a/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester.xcodeproj/project.pbxproj +++ b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester.xcodeproj/project.pbxproj @@ -6,15 +6,21 @@ objectVersion = 77; objects = { -/* Begin PBXBuildFile section */ - C0AD1A072F180C2B005A1DBD /* hermes.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0AD1A052F180C2B005A1DBD /* hermes.xcframework */; }; - C0AD1A0E2F182AAA005A1DBD /* minimaltesterbrownfield.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = C0AD1A0D2F182AAA005A1DBD /* minimaltesterbrownfield.xcframework */; }; -/* End PBXBuildFile section */ +/* Begin PBXCopyFilesBuildPhase section */ + 87F31DDB2F47363100B37DF8 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ C0AD19F12F180368005A1DBD /* BrownfieldIntegratedTester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BrownfieldIntegratedTester.app; sourceTree = BUILT_PRODUCTS_DIR; }; - C0AD1A052F180C2B005A1DBD /* hermes.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = hermes.xcframework; sourceTree = ""; }; - C0AD1A0D2F182AAA005A1DBD /* minimaltesterbrownfield.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = minimaltesterbrownfield.xcframework; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -30,8 +36,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - C0AD1A0E2F182AAA005A1DBD /* minimaltesterbrownfield.xcframework in Frameworks */, - C0AD1A072F180C2B005A1DBD /* hermes.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -43,8 +47,6 @@ children = ( C0AD19F32F180368005A1DBD /* BrownfieldIntegratedTester */, C0AD19F22F180368005A1DBD /* Products */, - C0AD1A052F180C2B005A1DBD /* hermes.xcframework */, - C0AD1A0D2F182AAA005A1DBD /* minimaltesterbrownfield.xcframework */, ); sourceTree = ""; }; @@ -66,6 +68,7 @@ C0AD19ED2F180368005A1DBD /* Sources */, C0AD19EE2F180368005A1DBD /* Frameworks */, C0AD19EF2F180368005A1DBD /* Resources */, + 87F31DDB2F47363100B37DF8 /* Embed Frameworks */, ); buildRules = ( ); diff --git a/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/BrownfieldIntegratedTesterApp.swift b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/BrownfieldIntegratedTesterApp.swift index 7fa5fbd8119321..fef64713546f30 100644 --- a/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/BrownfieldIntegratedTesterApp.swift +++ b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/BrownfieldIntegratedTesterApp.swift @@ -1,5 +1,5 @@ import SwiftUI -import minimaltesterbrownfield +import expoappbrownfield @main struct BrownfieldIntegratedTesterApp: App { diff --git a/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/BrownfieldTester.swift b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/BrownfieldTester.swift new file mode 100644 index 00000000000000..132af438c013ef --- /dev/null +++ b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/BrownfieldTester.swift @@ -0,0 +1,67 @@ +import Combine +import SwiftUI +import expoappbrownfield + +class BrownfieldTester: ObservableObject { + @Published var alertMessage: String = "" + @Published var showAlert: Bool = false + + // MARK: - Internal State + + private var listenerId: String? + private var messageTimer: Timer? + private var messageCounter = 0 + + // MARK: - Lifecycle Methods + + func start() { + setupListener() + startTimer() + } + + func stop() { + if let listenerId = listenerId { + BrownfieldMessaging.removeListener(id: listenerId) + } + messageTimer?.invalidate() + messageTimer = nil + } + + // MARK: - Private Logic + + private func setupListener() { + listenerId = BrownfieldMessaging.addListener { [weak self] message in + guard let self = self else { return } + + let sender = message["sender"] as? String ?? "Unknown" + let nested = message["source"] as? [String: Any?] ?? [:] + let platform = nested["platform"] as? String ?? "Unknown" + + DispatchQueue.main.async { + self.alertMessage = "\(platform)(\(sender))" + self.showAlert = true + print(self.alertMessage, self.showAlert) + } + } + } + + private func startTimer() { + messageTimer = Timer.scheduledTimer(withTimeInterval: 2.5, repeats: true) { [weak self] _ in + self?.sendMessage() + } + } + + private func sendMessage() { + messageCounter += 1 + + let nativeMessage: [String: Any] = [ + "source": ["platform": "iOS"], + "counter": messageCounter, + "timestamp": Int64(Date().timeIntervalSince1970 * 1000), + "array": ["ab", "c", false, 1, 2.45] as [Any] + ] + + BrownfieldMessaging.sendMessage(nativeMessage) + print("Sent: \(nativeMessage)") + } +} diff --git a/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/ContentView.swift b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/ContentView.swift index 1696389cb4edc1..158cf1089a9daf 100644 --- a/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/ContentView.swift +++ b/apps/brownfield-tester/ios-integrated/BrownfieldIntegratedTester/ContentView.swift @@ -1,16 +1,24 @@ import SwiftUI -import minimaltesterbrownfield +import expoappbrownfield struct ContentView: View { + @StateObject private var brownfieldTester = BrownfieldTester() + var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") - ReactNativeView(moduleName: "main") + NavigationStack { + NavigationLink(destination: ReactNativeView(moduleName: "main"), label: { + Text("Open React Native App") + .accessibilityIdentifier("openReactNativeButton") + .font(.largeTitle) + }) + } + .onAppear { brownfieldTester.start() } + .onDisappear { brownfieldTester.stop() } + .alert("Message from Native", isPresented: $brownfieldTester.showAlert) { + Button("OK", role: .cancel) { } + } message: { + Text(brownfieldTester.alertMessage) } - .padding() } } From 466c99577ce99426b2d5d10d092e2375d5e26274 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Thu, 19 Feb 2026 17:59:34 +0000 Subject: [PATCH 2/5] fix(expo/fetch): Align w/ method normalization, `Request`-like input, and `URL` input (#43194) # Why Resolves #43193 Resolves #43192 This fixes a few discrepancies in the `expo/fetch` implementation that were reported in the linked issues and further that I noticed when double-checking the against [`fetch-nodeshim`](https://github.com/kitten/fetch-nodeshim) (which I previously checked against the Fetch standard) # How - `method` needs to be normalised to uppercase (this isn't obvious in the standard; commit links to section) - input needs to accept `URL` and stringify it - input needs to accept `Request` and replace `init` with it - **NOTE:** Typically, only one argument is used in that case, but it's more convenient to express it as a "replacement." I've added a custom `FetchRequestLike` type to mirror `RequestInit` - `body` and `signal` need to accept `null` as valid input values in `FetchRequestInit` # Test Plan - Unclear how we typically test this, but could manually verify this; changes are pretty straightforward though - For type compatibility, `fetch(new URL('...'))` and `fetch(new Request('...'))` shouldn't raise type errors # Checklist - [x] 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) --- packages/expo/CHANGELOG.md | 2 + .../expo/build/winter/fetch/RequestUtils.d.ts | 2 + .../build/winter/fetch/RequestUtils.d.ts.map | 2 +- packages/expo/build/winter/fetch/fetch.d.ts | 4 +- .../expo/build/winter/fetch/fetch.d.ts.map | 2 +- .../expo/build/winter/fetch/fetch.types.d.ts | 16 ++++++- .../build/winter/fetch/fetch.types.d.ts.map | 2 +- .../expo/src/winter/fetch/RequestUtils.ts | 16 +++++++ packages/expo/src/winter/fetch/fetch.ts | 47 ++++++++++++++----- packages/expo/src/winter/fetch/fetch.types.ts | 19 +++++++- 10 files changed, 91 insertions(+), 21 deletions(-) diff --git a/packages/expo/CHANGELOG.md b/packages/expo/CHANGELOG.md index 94acf959207db5..ea9770c764d74c 100644 --- a/packages/expo/CHANGELOG.md +++ b/packages/expo/CHANGELOG.md @@ -8,6 +8,8 @@ ### 🐛 Bug fixes +- Add missing `Request`-like input handling, `method` normalization, and URL argument support to `fetch` ([#43194](https://github.com/expo/expo/pull/43194) by [@kitten](https://github.com/kitten)) + ### 💡 Others ## 55.0.0-preview.11 — 2026-02-16 diff --git a/packages/expo/build/winter/fetch/RequestUtils.d.ts b/packages/expo/build/winter/fetch/RequestUtils.d.ts index 3b76e0acaffc5b..ac611055061b1e 100644 --- a/packages/expo/build/winter/fetch/RequestUtils.d.ts +++ b/packages/expo/build/winter/fetch/RequestUtils.d.ts @@ -18,4 +18,6 @@ export declare function normalizeHeadersInit(headers: HeadersInit | null | undef * Create a new header array by overriding the existing headers with new headers (by header key). */ export declare function overrideHeaders(headers: NativeHeadersType, newHeaders: NativeHeadersType): NativeHeadersType; +/** Normalizes known HTTP methods to uppercase */ +export declare function normalizeMethod(method: string): string; //# sourceMappingURL=RequestUtils.d.ts.map \ No newline at end of file diff --git a/packages/expo/build/winter/fetch/RequestUtils.d.ts.map b/packages/expo/build/winter/fetch/RequestUtils.d.ts.map index 64bb401476d7a9..59db63fcedc106 100644 --- a/packages/expo/build/winter/fetch/RequestUtils.d.ts.map +++ b/packages/expo/build/winter/fetch/RequestUtils.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"RequestUtils.d.ts","sourceRoot":"","sources":["../../../src/winter/fetch/RequestUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAIzD;;GAEG;AACH,wBAAsB,sCAAsC,CAC1D,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,GACjC,OAAO,CAAC,UAAU,CAAC,CAsBrB;AAgBD;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,GAChC,OAAO,CAAC;IAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;CAAE,CAAC,CA6C7E;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,iBAAiB,CAe/F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,iBAAiB,EAC1B,UAAU,EAAE,iBAAiB,GAC5B,iBAAiB,CAYnB"} \ No newline at end of file +{"version":3,"file":"RequestUtils.d.ts","sourceRoot":"","sources":["../../../src/winter/fetch/RequestUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAIzD;;GAEG;AACH,wBAAsB,sCAAsC,CAC1D,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,GACjC,OAAO,CAAC,UAAU,CAAC,CAsBrB;AAgBD;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,GAChC,OAAO,CAAC;IAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CAAC;IAAC,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;CAAE,CAAC,CA6C7E;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,GAAG,iBAAiB,CAe/F;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,iBAAiB,EAC1B,UAAU,EAAE,iBAAiB,GAC5B,iBAAiB,CAYnB;AAED,iDAAiD;AACjD,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAatD"} \ No newline at end of file diff --git a/packages/expo/build/winter/fetch/fetch.d.ts b/packages/expo/build/winter/fetch/fetch.d.ts index 94053b955a1af9..bf49e54ec83c71 100644 --- a/packages/expo/build/winter/fetch/fetch.d.ts +++ b/packages/expo/build/winter/fetch/fetch.d.ts @@ -1,4 +1,4 @@ import { FetchResponse } from './FetchResponse'; -import type { FetchRequestInit } from './fetch.types'; -export declare function fetch(url: string, init?: FetchRequestInit): Promise; +import type { FetchRequestInit, FetchRequestLike } from './fetch.types'; +export declare function fetch(input: string | URL | FetchRequestLike, init?: FetchRequestInit): Promise; //# sourceMappingURL=fetch.d.ts.map \ No newline at end of file diff --git a/packages/expo/build/winter/fetch/fetch.d.ts.map b/packages/expo/build/winter/fetch/fetch.d.ts.map index dbd1bfdbbdbb86..9d76f859d3820d 100644 --- a/packages/expo/build/winter/fetch/fetch.d.ts.map +++ b/packages/expo/build/winter/fetch/fetch.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../src/winter/fetch/fetch.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAyC,MAAM,iBAAiB,CAAC;AAGvF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAGtD,wBAAsB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,aAAa,CAAC,CAsCxF"} \ No newline at end of file +{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../src/winter/fetch/fetch.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAyC,MAAM,iBAAiB,CAAC;AAQvF,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAOxE,wBAAsB,KAAK,CACzB,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,gBAAgB,EACtC,IAAI,CAAC,EAAE,gBAAgB,GACtB,OAAO,CAAC,aAAa,CAAC,CAiDxB"} \ No newline at end of file diff --git a/packages/expo/build/winter/fetch/fetch.types.d.ts b/packages/expo/build/winter/fetch/fetch.types.d.ts index c2f9fb4c97e038..fc83ef19c53819 100644 --- a/packages/expo/build/winter/fetch/fetch.types.d.ts +++ b/packages/expo/build/winter/fetch/fetch.types.d.ts @@ -2,11 +2,11 @@ * A fetch RequestInit compatible structure. */ export interface FetchRequestInit { - body?: BodyInit; + body?: BodyInit | null; credentials?: RequestCredentials; headers?: HeadersInit; method?: string; - signal?: AbortSignal; + signal?: AbortSignal | null; redirect?: RequestRedirect; integrity?: string; keepalive?: boolean; @@ -14,4 +14,16 @@ export interface FetchRequestInit { referrer?: string; window?: any; } +/** + * A fetch Request compatible structure. + */ +export interface FetchRequestLike { + readonly url: string; + readonly body: BodyInit | null; + readonly method: string; + readonly headers: Headers; + readonly credentials?: RequestCredentials; + readonly signal?: AbortSignal; + readonly redirect?: RequestRedirect; +} //# sourceMappingURL=fetch.types.d.ts.map \ No newline at end of file diff --git a/packages/expo/build/winter/fetch/fetch.types.d.ts.map b/packages/expo/build/winter/fetch/fetch.types.d.ts.map index b7ed9c54b276f1..69002a736edada 100644 --- a/packages/expo/build/winter/fetch/fetch.types.d.ts.map +++ b/packages/expo/build/winter/fetch/fetch.types.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"fetch.types.d.ts","sourceRoot":"","sources":["../../../src/winter/fetch/fetch.types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,QAAQ,CAAC,EAAE,eAAe,CAAC;IAG3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,GAAG,CAAC;CACd"} \ No newline at end of file +{"version":3,"file":"fetch.types.d.ts","sourceRoot":"","sources":["../../../src/winter/fetch/fetch.types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IACvB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,EAAE,eAAe,CAAC;IAG3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,GAAG,CAAC;CACd;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAG1B,QAAQ,CAAC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IAC1C,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;CACrC"} \ No newline at end of file diff --git a/packages/expo/src/winter/fetch/RequestUtils.ts b/packages/expo/src/winter/fetch/RequestUtils.ts index c731629e9210d0..4dabadd08777b8 100644 --- a/packages/expo/src/winter/fetch/RequestUtils.ts +++ b/packages/expo/src/winter/fetch/RequestUtils.ts @@ -136,3 +136,19 @@ export function overrideHeaders( } return result; } + +/** Normalizes known HTTP methods to uppercase */ +export function normalizeMethod(method: string): string { + const normalized = method.toUpperCase(); + switch (method.toUpperCase()) { + case 'DELETE': + case 'GET': + case 'HEAD': + case 'OPTIONS': + case 'POST': + case 'PUT': + return normalized; + default: + return method; + } +} diff --git a/packages/expo/src/winter/fetch/fetch.ts b/packages/expo/src/winter/fetch/fetch.ts index ebdda4389d488b..47f76872bd8f7f 100644 --- a/packages/expo/src/winter/fetch/fetch.ts +++ b/packages/expo/src/winter/fetch/fetch.ts @@ -2,40 +2,63 @@ import { ExpoFetchModule } from './ExpoFetchModule'; import { FetchError } from './FetchErrors'; import { FetchResponse, type AbortSubscriptionCleanupFunction } from './FetchResponse'; import { NativeRequest, NativeRequestInit } from './NativeRequest'; -import { normalizeBodyInitAsync, normalizeHeadersInit, overrideHeaders } from './RequestUtils'; -import type { FetchRequestInit } from './fetch.types'; +import { + normalizeBodyInitAsync, + normalizeHeadersInit, + overrideHeaders, + normalizeMethod, +} from './RequestUtils'; +import type { FetchRequestInit, FetchRequestLike } from './fetch.types'; + +/** Returns if `input` is a Request object */ +const isRequest = (input: any): input is FetchRequestLike => + input != null && typeof input === 'object' && 'body' in input; // TODO(@kitten): Do we really want to use our own types for web standards? -export async function fetch(url: string, init?: FetchRequestInit): Promise { +export async function fetch( + input: string | URL | FetchRequestLike, + init?: FetchRequestInit +): Promise { + const initFromRequest = isRequest(input); + const url = initFromRequest ? input.url : input; + const body = init?.body ?? (initFromRequest ? input.body : null); + const signal = init?.signal ?? (initFromRequest ? input.signal : undefined); + const redirect = init?.redirect ?? (initFromRequest ? input.redirect : undefined); + const method = init?.method ?? (initFromRequest ? input.method : undefined); + const credentials = init?.credentials ?? (initFromRequest ? input.credentials : undefined); + + let headers = normalizeHeadersInit( + init?.headers ?? (initFromRequest ? input.headers : undefined) + ); + let abortSubscription: AbortSubscriptionCleanupFunction | null = null; const response = new FetchResponse(() => { abortSubscription?.(); }); - const request = new ExpoFetchModule.NativeRequest(response) as NativeRequest; - let headers = normalizeHeadersInit(init?.headers); + const request = new ExpoFetchModule.NativeRequest(response) as NativeRequest; - const { body: requestBody, overriddenHeaders } = await normalizeBodyInitAsync(init?.body); + const { body: requestBody, overriddenHeaders } = await normalizeBodyInitAsync(body); if (overriddenHeaders) { headers = overrideHeaders(headers, overriddenHeaders); } const nativeRequestInit: NativeRequestInit = { - credentials: init?.credentials ?? 'include', + credentials: credentials ?? 'include', headers, - method: init?.method ?? 'GET', - redirect: init?.redirect ?? 'follow', + method: method != null ? normalizeMethod(method) : 'GET', + redirect: redirect ?? 'follow', }; - if (init?.signal && init.signal.aborted) { + if (signal && signal.aborted) { throw new FetchError('The operation was aborted.'); } - abortSubscription = addAbortSignalListener(init?.signal, () => { + abortSubscription = addAbortSignalListener(signal, () => { request.cancel(); }); try { - await request.start(url, nativeRequestInit, requestBody); + await request.start(`${url}`, nativeRequestInit, requestBody); } catch (e: unknown) { if (e instanceof Error) { throw FetchError.createFromError(e); diff --git a/packages/expo/src/winter/fetch/fetch.types.ts b/packages/expo/src/winter/fetch/fetch.types.ts index 7ae525e7a31230..70f4f0e67ca7cb 100644 --- a/packages/expo/src/winter/fetch/fetch.types.ts +++ b/packages/expo/src/winter/fetch/fetch.types.ts @@ -2,11 +2,11 @@ * A fetch RequestInit compatible structure. */ export interface FetchRequestInit { - body?: BodyInit; + body?: BodyInit | null; credentials?: RequestCredentials; // same-origin is not supported headers?: HeadersInit; method?: string; - signal?: AbortSignal; + signal?: AbortSignal | null; redirect?: RequestRedirect; // Not supported fields @@ -16,3 +16,18 @@ export interface FetchRequestInit { referrer?: string; window?: any; } + +/** + * A fetch Request compatible structure. + */ +export interface FetchRequestLike { + readonly url: string; + readonly body: BodyInit | null; + readonly method: string; + readonly headers: Headers; + + // Not always supported, marked as optional + readonly credentials?: RequestCredentials; + readonly signal?: AbortSignal; + readonly redirect?: RequestRedirect; +} From fde9c9febf03e332e5f4c64c31468fafc5750b2f Mon Sep 17 00:00:00 2001 From: Aman Mittal Date: Fri, 20 Feb 2026 01:08:13 +0530 Subject: [PATCH 3/5] [docs] Strip UI-only frontmatter fields from LLM markdown generation (#43211) ## Why Fix ENG-19537 ## How - Added a `UI_ONLY_FRONTMATTER_FIELDS` set in `scripts/generate-markdown-pages-utils.ts` listing the 8 fields to strip: `hideTOC`, `maxHeadingDepth`, `hideFromSearch`, `hideInSidebar`, `sidebar_title`, `searchRank`, `searchPosition`, `hasVideoLink`. - Added a second `.filter()` step in `extractFrontmatter()` that checks each YAML line's key against the set and drops matches. - Added two test cases in `scripts/generate-markdown-pages-utils.test.ts`: one verifying UI-only fields are stripped while semantic fields (`isDeprecated`, `isAlpha`) are preserved, and one verifying that frontmatter containing only UI-only fields returns `null`. - Semantic fields like `title`, `description`, `modificationDate`, `isDeprecated`, `isAlpha`, `isNew`, and `cliVersion` are preserved since they provide useful signals. ## Test Plan Run `yarn run generate-markdown-pages` then `yarn run generate-llms` - Verify that `llms-eas.txt`, `llms-full.txt`, and `llms-sdk.txt` no longer contain `hideTOC`, `maxHeadingDepth`, `sidebar_title`, `searchRank`, `searchPosition`, `hideFromSearch`, `hideInSidebar`, or `hasVideoLink` in frontmatter blocks. - Confirm that `title`, `description`, `modificationDate`, `isDeprecated`, `isAlpha`, and `isNew` are still present. CleanShot 2026-02-17 at 19 39 53@2x CleanShot 2026-02-17 at 19 40 23@2x **Alternatively, you can run `yarn export` and `npx serve out`** and visit pages like "Create and use config plugins" and "Using existing credentials" and click View Markdown button on these pages. ## 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) --- .../generate-markdown-pages-utils.test.ts | 54 +++++++++++++++++++ docs/scripts/generate-markdown-pages-utils.ts | 24 ++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/docs/scripts/generate-markdown-pages-utils.test.ts b/docs/scripts/generate-markdown-pages-utils.test.ts index 31a2201e8e3e8f..adb22ff75af628 100644 --- a/docs/scripts/generate-markdown-pages-utils.test.ts +++ b/docs/scripts/generate-markdown-pages-utils.test.ts @@ -1378,6 +1378,35 @@ describe('extractFrontmatter', () => { path.join(tmpDir, 'no-frontmatter.mdx'), "import Foo from './Foo';\n\n# Hello\n" ); + + fs.writeFileSync( + path.join(tmpDir, 'ui-fields.mdx'), + [ + '---', + 'title: Camera', + 'description: A camera component.', + 'hideTOC: true', + 'maxHeadingDepth: 4', + 'hideFromSearch: true', + 'hideInSidebar: true', + 'sidebar_title: Cam', + 'searchRank: 10', + 'searchPosition: 5', + 'hasVideoLink: true', + 'packageName: expo-camera', + 'isDeprecated: true', + 'isAlpha: true', + '---', + '', + '# Camera', + '', + ].join('\n') + ); + + fs.writeFileSync( + path.join(tmpDir, 'only-ui-fields.mdx'), + '---\nhideTOC: true\nmaxHeadingDepth: 4\n---\n\n# Page\n' + ); }); afterAll(() => { @@ -1421,4 +1450,29 @@ describe('extractFrontmatter', () => { const result = extractFrontmatter(path.join(tmpDir, 'no-frontmatter.mdx')); expect(result).toBeNull(); }); + + it('strips UI-only fields and keeps semantic fields', () => { + const result = extractFrontmatter(path.join(tmpDir, 'ui-fields.mdx')); + expect(result).not.toBeNull(); + // Semantic fields are preserved + expect(result).toContain('title: Camera'); + expect(result).toContain('description: A camera component.'); + expect(result).toContain('isDeprecated: true'); + expect(result).toContain('isAlpha: true'); + expect(result).toContain('packageName: expo-camera'); + // UI-only fields are stripped + expect(result).not.toContain('hideTOC'); + expect(result).not.toContain('maxHeadingDepth'); + expect(result).not.toContain('hideFromSearch'); + expect(result).not.toContain('hideInSidebar'); + expect(result).not.toContain('sidebar_title'); + expect(result).not.toContain('searchRank'); + expect(result).not.toContain('searchPosition'); + expect(result).not.toContain('hasVideoLink'); + }); + + it('returns null when all fields are UI-only', () => { + const result = extractFrontmatter(path.join(tmpDir, 'only-ui-fields.mdx')); + expect(result).toBeNull(); + }); }); diff --git a/docs/scripts/generate-markdown-pages-utils.ts b/docs/scripts/generate-markdown-pages-utils.ts index 0f7527a0aba89b..b512bf8cdfe00c 100644 --- a/docs/scripts/generate-markdown-pages-utils.ts +++ b/docs/scripts/generate-markdown-pages-utils.ts @@ -23,10 +23,28 @@ export function findMdxSource(htmlPath: string, outDir: string, pagesDir: string return null; } +/** + * Frontmatter fields that only affect the docs website UI (sidebar, TOC, search ranking) + * and carry no semantic value for LLM or MCP consumers. Stripped during markdown generation. + * + * Note: `packageName` is intentionally kept because the Expo docs MCP tool uses it + * to map pages to their npm packages. + */ +const UI_ONLY_FRONTMATTER_FIELDS = new Set([ + 'hideTOC', + 'maxHeadingDepth', + 'hideFromSearch', + 'hideInSidebar', + 'sidebar_title', + 'searchRank', + 'searchPosition', + 'hasVideoLink', +]); + /** * Extract the raw YAML frontmatter block (including --- delimiters) from an MDX file. * Strips lines with empty values (e.g. `modificationDate:` injected by append-dates.js - * with no value in shallow CI clones). + * with no value in shallow CI clones) and UI-only fields that are irrelevant to LLM consumers. * Returns the frontmatter string with trailing newline, or null if no frontmatter found. */ export function extractFrontmatter(mdxPath: string): string | null { @@ -38,6 +56,10 @@ export function extractFrontmatter(mdxPath: string): string | null { const filtered = match[1] .split('\n') .filter(line => !/^\w+:\s*$/.test(line)) + .filter(line => { + const key = line.match(/^(\w+):/)?.[1]; + return !key || !UI_ONLY_FRONTMATTER_FIELDS.has(key); + }) .join('\n'); if (!filtered.trim()) { return null; From bdddd5110a46e24f90a76999d8abde5cc0254c8f Mon Sep 17 00:00:00 2001 From: Aman Mittal Date: Fri, 20 Feb 2026 01:08:33 +0530 Subject: [PATCH 4/5] [docs] Clarify differences between run commands and expo start (#43224) ## Why Fix ENG-10293 Developers who build locally with `npx expo run:android` or `npx expo run:ios` often keep using those commands for every iteration, waiting a minute or more for native compilation when they only changed JavaScript code. The existing page documents the `run` commands but never explains that `npx expo start` is the faster workflow for daily development after the first build. ## How - Expand the explanation of what `run` commands do to clarify that they perform two steps: compile and install the native binary, then start Metro - Add a new `After the first build: use npx expo start` subsection with the practical workflow pattern - Add a comparison table showing when to use `run` vs `start` and what each command does. - Fix broken anchor links and link to CNG and prebuild documentation. ## Test Plan - Open the link locally: http://localhost:3002/guides/local-app-development/#after-the-first-build-use-npx-expo-start or open preview link: https://pr-43224.expo-docs.pages.dev/guides/local-app-development/#after-the-first-build-use-npx-expo-start - Confirm the new "After the first build" subsection renders correctly with the Terminal component and comparison table. Click all internal links on the page and verify none are broken or redirect-based. # 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/guides/local-app-development.mdx | 25 ++++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/docs/pages/guides/local-app-development.mdx b/docs/pages/guides/local-app-development.mdx index 4f2e735d27b28d..0ad14a26501179 100644 --- a/docs/pages/guides/local-app-development.mdx +++ b/docs/pages/guides/local-app-development.mdx @@ -33,16 +33,29 @@ To build your project locally you can use compile commands from Expo CLI which g ]} /> -The above commands compile your project, using your locally installed Android SDK or Xcode, into a debug build of your app. +The above commands compile your project, using your locally installed Android SDK or Xcode, into a debug build of your app. Each command performs two steps: it compiles and installs the native binary on your device or emulator, then starts the Metro bundler to serve your JavaScript or TypeScript code. - These compilation commands initially run `npx expo prebuild` to generate native directories (**android** and **ios**) before building, if they do not exist yet. If they already exist, this will be skipped. - You can also add the `--device` flag to select a device to run the app on — you can select a physically connected device or emulator/simulator. -- You can pass in `--variant release` (Android) or `--configuration Release` (iOS) to build a [production build of your app](/deploy/build-project/#production-builds-locally). Note that these builds are not signed and you cannot submit them to app stores. To sign your production build, see [Local app production](/guides/local-app-production/). +- You can pass in `--variant release` (Android) or `--configuration Release` (iOS) to build a [production build of your app](/deploy/build-project/#release-builds-locally). Note that these builds are not signed and you cannot submit them to app stores. To sign your production build, see [Local app production](/guides/local-app-production/). - **Android only**: Starting in SDK 54, you can pass the `--variant debugOptimized` variant for faster development iteration. See [Compiling Android in Expo CLI reference](/more/expo-cli/#compiling-android) for more information. -To modify your project's configuration or native code after the first build, you will have to rebuild your project. Running `npx expo prebuild` again layers the changes on top of existing files. It may also produce different results after the build. +### After the first build: use `npx expo start` -To avoid this, the native directories are automatically added to the project's **.gitignore** when you create a new project, and you can use `npx expo prebuild --clean` command. This ensures that the project is always managed, and the [`--clean` flag](/workflow/prebuild/#clean) will delete existing directories before regenerating them. You can use [app config](/workflow/configuration/) or create a [config plugin](/config-plugins/introduction/) to modify your project's configuration or code inside the native directories. +Once the app is compiled and installed on your device or emulator, you don't need to rebuild every time you make a change. If you're only modifying JavaScript or TypeScript code, you can start the Metro bundler on its own: + + + +Then press a for Android or i for iOS in the terminal to launch the already-installed app. Metro serves your updated JavaScript bundle without recompiling native code, so the app loads in seconds instead of minutes. + +| Command | What it does | When to use it | +| ------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------- | +| `npx expo run:android` / `npx expo run:ios` | Compiles native code, installs the app, starts Metro. | First build, after adding a native library, or after modifying a config plugin. | +| `npx expo start` | Starts only the Metro bundler. | Daily development when only changing JavaScript or TypeScript code. | + +To modify your project's configuration or native code after the first build, you will have to rebuild your project using `npx expo run:android|ios` again. Running `npx expo prebuild` again layers the changes on top of existing files. It may also produce different results after the build. + +To avoid this, the native directories are automatically added to the project's **.gitignore** when you create a new project, and you can use `npx expo prebuild --clean` command. This ensures that the project is always managed, and the [`--clean` flag](/workflow/continuous-native-generation/#clean) will delete existing directories before regenerating them. You can use [app config](/workflow/configuration/) or create a [config plugin](/config-plugins/introduction/) to modify your project's configuration or code inside the native directories. To learn more about how compilation and prebuild works, see the following guides: @@ -61,13 +74,13 @@ To learn more about how compilation and prebuild works, see the following guides ## Local builds with `expo-dev-client` -If you install [`expo-dev-client`](/develop/development-builds/introduction/#what-is-expo-dev-client) to your project, then a debug build of your project will include the `expo-dev-client` UI and tooling, and we call these development builds. +If you install [`expo-dev-client`](/develop/development-builds/introduction/) to your project, then a debug build of your project will include the `expo-dev-client` UI and tooling, and we call these development builds. From 9ea69398c96682f3c2ef61e544cef1405cdd8c9b Mon Sep 17 00:00:00 2001 From: Abel <59095867+BlessedOneKobo@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:07:36 +0100 Subject: [PATCH 5/5] [docs] Fix tap gesture API documentation link (#43231) # Why The current link is broken (404) # How This link points to docs for the 2.x version of the library. The current library version (3.x) uses hooks for tap gestures which is different from the tutorial # Test Plan N/A # 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/tutorial/gestures.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pages/tutorial/gestures.mdx b/docs/pages/tutorial/gestures.mdx index 93dd262bc30f61..7a8193da020868 100644 --- a/docs/pages/tutorial/gestures.mdx +++ b/docs/pages/tutorial/gestures.mdx @@ -196,7 +196,7 @@ Let's take a look at our app on Android, iOS and the web: -> For a complete reference of the tap gesture API, see the [React Native Gesture Handler](https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/tap-gesture) documentation. +> For a complete reference of the tap gesture API, see the [React Native Gesture Handler](https://docs.swmansion.com/react-native-gesture-handler/docs/2.x/gestures/tap-gesture) documentation.