Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import SwiftUI
import minimaltesterbrownfield
import expoappbrownfield

@main
struct BrownfieldIntegratedTesterApp: App {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)")
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}

Expand Down
25 changes: 19 additions & 6 deletions docs/pages/guides/local-app-development.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:

<Terminal cmd={['$ npx expo start']} />

Then press <kbd>a</kbd> for Android or <kbd>i</kbd> 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:

Expand All @@ -61,13 +74,13 @@ To learn more about how compilation and prebuild works, see the following guides
<BoxLink
title="Prebuild"
description="Learn how Expo CLI generates native code of your project before compiling it."
href="/workflow/prebuild"
href="/workflow/continuous-native-generation"
Icon={BookOpen02Icon}
/>

## 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.

<Terminal cmd={['$ npx expo install expo-dev-client']} />

Expand Down
2 changes: 1 addition & 1 deletion docs/pages/tutorial/gestures.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ Let's take a look at our app on Android, iOS and the web:

<ContentSpotlight file="tutorial/tap-gesture.mp4" />

> 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.

</Step>

Expand Down
54 changes: 54 additions & 0 deletions docs/scripts/generate-markdown-pages-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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();
});
});
24 changes: 23 additions & 1 deletion docs/scripts/generate-markdown-pages-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions packages/expo/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/expo/build/winter/fetch/RequestUtils.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/expo/build/winter/fetch/RequestUtils.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/expo/build/winter/fetch/fetch.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/expo/build/winter/fetch/fetch.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 14 additions & 2 deletions packages/expo/build/winter/fetch/fetch.types.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading