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() } } 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. 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. 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; 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; +}