agents/openai.yaml
interface:
display_name: "Expo Module"
short_description: "Build Expo native modules and native views with Swift, Kotlin, TypeScript, autolinking, and config plugins"
default_prompt: "Use $expo-module to scaffold, implement, clean up, or review Expo native modules, native views, shared objects, lifecycle hooks, autolinking configuration, and config plugins."
references/config-plugin.md
# Config Plugins Reference
Config plugins customize native Android and iOS projects generated with `npx expo prebuild`. They are synchronous functions that accept an `ExpoConfig` and return a modified version.
## Plugin Structure
```
my-module/
plugin/
tsconfig.json
src/
index.ts
app.plugin.js # Entry: module.exports = require('./plugin/build');
```
## Writing a Plugin
Plugin functions follow the `with` prefix naming convention.
```typescript
import {
ConfigPlugin,
withInfoPlist,
withAndroidManifest,
AndroidConfig,
} from "expo/config-plugins";
const withMyConfig: ConfigPlugin<{ apiKey: string }> = (config, { apiKey }) => {
// iOS: modify Info.plist
config = withInfoPlist(config, (config) => {
config.modResults["MY_API_KEY"] = apiKey;
return config;
});
// Android: modify AndroidManifest.xml
config = withAndroidManifest(config, (config) => {
const mainApp =
AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults);
AndroidConfig.Manifest.addMetaDataItemToMainApplication(
mainApp,
"MY_API_KEY",
apiKey
);
return config;
});
return config;
};
export default withMyConfig;
```
## Using in app.json
```json
{
"expo": {
"plugins": [["my-module", { "apiKey": "secret_key" }]]
}
}
```
## Reading Config Values in Native Code
**Swift:**
```swift
Function("getApiKey") {
return Bundle.main.object(forInfoDictionaryKey: "MY_API_KEY") as? String
}
```
**Kotlin:**
```kotlin
Function("getApiKey") {
val appInfo = appContext?.reactContext?.packageManager?.getApplicationInfo(
appContext?.reactContext?.packageName.toString(),
PackageManager.GET_META_DATA
)
return@Function appInfo?.metaData?.getString("MY_API_KEY")
}
```
## Key Rules
- Plugins must be synchronous; return values must be serializable (except `mods`)
- `Mods` are async functions invoked during the prebuild "syncing" phase
- Use `npm run build plugin` to compile TypeScript plugins
- Test with `npx expo prebuild --clean`
references/create-expo-module.md
# create-expo-module
Use `create-expo-module` to scaffold new Expo modules and `create-expo-module add-platform-support` to extend existing Expo modules.
Prefer `create-expo-module` over manually creating module files and directories. In most cases, the right move is to generate the scaffold first and then build on top of it.
## Choose the Module Type First
### Local module
Use a local module when the native code only belongs to one Expo app.
- lives inside the app
- uses the app's dependencies and tooling
- does not create an example app
- respects `package.json:expo.autolinking.nativeModulesDir`, or falls back to `modules/`
### Standalone module
Use a standalone module when the module should be reusable across apps, live in a monorepo package, or be published to npm.
- has its own `package.json`
- installs its own dependencies
- builds TypeScript during scaffolding
- usually creates an `example` app unless `--no-example` is passed
- may initialize a Git repo if not already inside one
When creating a standalone module, default to keeping the example app. Only skip it when the user explicitly asks for `--no-example` or clearly does not want the example project.
## Recommended Commands
### Local module
Use an explicit slug or path.
```bash
npx create-expo-module@latest key-value-store --local --platform apple android --features Function AsyncFunction
```
If you need deterministic non-interactive output, pass the slug or path explicitly and then pass the rest of the options:
```bash
EXPO_NONINTERACTIVE=1 npx create-expo-module@latest key-value-store \
--local \
--name KeyValueStore \
--package expo.modules.keyvaluestore \
--platform apple android \
--features Function AsyncFunction
```
Important quirk:
- in non-interactive local scaffolding, omitting the positional slug causes the CLI to fall back to `my-module`
- `--name` changes the native module class name, not the directory name
### Standalone module
```bash
npx create-expo-module@latest expo-key-value-store --platform apple android --features Function AsyncFunction
```
## Creation Options
These are the module creation options exposed by the CLI:
| Option | Applies to | Notes |
| ----------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------- |
| `[path]` | local, standalone | Positional slug or target path. Use this explicitly for stable local scaffolding in non-interactive mode. |
| `--local` | local | Create a local module inside the current Expo project. |
| `--platform <platforms...>` | local, standalone | Valid values: `apple`, `android`, `web`. |
| `--features <features...>` | local, standalone | Pick generated feature examples. Use `all` to include everything. |
| `--full-example` | local, standalone | Equivalent to `--features all`. |
| `--barrel` | local | Generate a local `index.ts` barrel. Ignored for standalone modules. |
| `--source <source_dir>` | local, standalone | Use a local `expo-module-template` directory instead of downloading from npm. |
| `--name <name>` | local, standalone | Native module name, for example `KeyValueStore`. |
| `--description <description>` | standalone | Package description. |
| `--package <package>` | local, standalone | Android package name, for example `expo.modules.keyvaluestore`. |
| `--author-name <name>` | standalone | Package author name. |
| `--author-email <email>` | standalone | Package author email. |
| `--author-url <url>` | standalone | Package author profile URL. |
| `--repo <url>` | standalone | Package repository URL. |
| `--license <license>` | standalone | Package license identifier. |
| `--module-version <version>` | standalone | Initial package version. |
| `--package-manager <manager>` | standalone | One of `npm`, `pnpm`, `yarn`, `bun`. |
| `--with-readme` | standalone | Keep `README.md` in the generated package. |
| `--with-changelog` | standalone | Keep `CHANGELOG.md` in the generated package. |
| `--no-example` | standalone | Skip creating the example app. |
Notes:
- `--description`, author flags, `--repo`, `--license`, and `--module-version` only affect standalone modules because local modules do not have a standalone package manifest.
- `--with-readme`, `--with-changelog`, `--no-example`, and `--package-manager` are standalone-only concerns.
- `--barrel` only affects local modules.
- `--name` changes the native module class name. It does not rename the local module directory.
## Platforms
Valid values are:
- `apple`
- `android`
- `web`
Behavior to remember:
- standalone modules default to all platforms when `--platform` is omitted in non-interactive mode
- local modules also default to all platforms in non-interactive mode
- interactive local scaffolding preselects platforms from `app.json:expo.platforms` when available, mapping `ios` to `apple`
- invalid platform values are ignored with a warning; if all provided values are invalid, the CLI falls back to all platforms
If you do not want web support, omit `web` during scaffolding instead of removing it later.
## Feature Examples
Feature examples are generated starter snippets, not capability restrictions. They are small working examples of common Expo Modules API patterns.
Available values:
- `Constant`
- `Function`
- `AsyncFunction`
- `Event`
- `View`
- `ViewEvent`
- `SharedObject`
Important behaviors:
- no features selected means a minimal module
- in interactive mode, feature examples start unselected
- in non-interactive mode, no features are included unless `--features` or `--full-example` is passed
- `--full-example` is equivalent to `--features all`
- `ViewEvent` automatically includes `View`
Use `View` only when the module actually renders UI. For native-only modules, do not scaffold the view files unless you plan to use them.
## Local Module Quirks
- local modules do not generate an `index.ts` barrel by default
- use `--barrel` only if you want a root barrel file
- local modules skip dependency installation and do not create an `example` app
- local modules do not have a local `package.json`; they rely on the host app
If `--barrel` is not used, the CLI's follow-up instructions point to direct imports from the module's `src/` files.
## Standalone Module Quirks
- `--package-manager` is only relevant for standalone modules
- if omitted, the CLI detects the package manager from the user agent or available package managers
- the scaffold builds TypeScript after installing dependencies
- the `example` app is created only when examples are enabled; `--no-example` skips it
- `--with-readme` and `--with-changelog` opt into those files
The generated standalone scripts include `build`, `clean`, `test`, `prepare`, `open:ios`, and `open:android`.
## add-platform-support
Use this subcommand when an existing Expo module needs another supported platform.
Interactive usage from the module root:
```bash
npx create-expo-module@latest add-platform-support
```
Explicit usage:
```bash
npx create-expo-module@latest add-platform-support --platform android
```
You can also pass the module path:
```bash
npx create-expo-module@latest add-platform-support ./packages/expo-key-value-store --platform web
```
Important behaviors:
- supported values are `apple`, `android`, and `web`
- in non-interactive mode, `--platform` is required
- the command only adds platforms that are not already present in `expo-module.config.json`
- it refuses to overwrite existing `android/` or `ios/` directories
- for native modules, it only works with modules that use the Expo Modules API DSL
- older module formats are not supported
### Feature detection
`add-platform-support` tries to detect the existing module's feature examples from the native module definition. This is best effort.
Use `--features` to override the detected feature examples when:
- the module is unusual
- the module uses generated code
- the definition is spread across multiple files
- the generated files do not match the existing module's shape
If no features are detected or provided, the command creates a minimal scaffold for the new platform.
## Environment Variables
- `EXPO_BETA`: use the next template version
- `EXPO_DEBUG`: enable debug logs
- `EXPO_NO_TELEMETRY`: disable telemetry
- `EXPO_NONINTERACTIVE`: force non-interactive mode
- `CI`: same as `EXPO_NONINTERACTIVE`, used in CI environments
references/lifecycle.md
# Lifecycle Hooks Reference
## Module Lifecycle (in module definition)
```swift
OnCreate {
// Module initialized - preferred over class initializers
}
OnDestroy {
// Module deallocated - clean up resources
}
OnAppContextDestroys {
// App context is being deallocated
}
```
## iOS App Lifecycle (in module definition)
```swift
OnAppEntersForeground { /* UIApplication.willEnterForegroundNotification */ }
OnAppEntersBackground { /* UIApplication.didEnterBackgroundNotification */ }
OnAppBecomesActive { /* UIApplication.didBecomeActiveNotification */ }
```
## Android Activity Lifecycle (in module definition)
```kotlin
OnActivityEntersForeground { /* Activity resumed */ }
OnActivityEntersBackground { /* Activity paused */ }
OnActivityDestroys { /* Activity destroyed */ }
OnNewIntent { intent -> /* Deep link received */ }
OnActivityResult { activity, result -> /* startActivityForResult callback */ }
OnUserLeavesActivity { /* User-initiated background transition */ }
RegisterActivityContracts { /* Modern activity result contracts */ }
```
---
## iOS AppDelegate Subscribers
For hooking into AppDelegate events without editing AppDelegate directly. Requires app's AppDelegate to extend `ExpoAppDelegate`.
```swift
import ExpoModulesCore
public class MyAppDelegateSubscriber: ExpoAppDelegateSubscriber {
public func applicationDidBecomeActive(_ application: UIApplication) {}
public func applicationWillResignActive(_ application: UIApplication) {}
public func applicationDidEnterBackground(_ application: UIApplication) {}
public func applicationWillEnterForeground(_ application: UIApplication) {}
public func applicationWillTerminate(_ application: UIApplication) {}
public func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return false // Return true if handled
}
}
```
Register in `expo-module.config.json`:
```json
{
"apple": {
"appDelegateSubscribers": ["MyAppDelegateSubscriber"]
}
}
```
Result aggregation:
- `didFinishLaunchingWithOptions`: Returns `true` if **any** subscriber returns `true`
- `didReceiveRemoteNotification`: Priority: `failed` > `newData` > `noData`
---
## Android Lifecycle Listeners
For hooking into Activity/Application lifecycle outside module definitions. Useful for handling deep links, intents, and app-level initialization.
### ReactActivityLifecycleListener
Supported callbacks: `onCreate`, `onResume`, `onPause`, `onDestroy`, `onNewIntent`, `onBackPressed`.
> Note: `onStart` and `onStop` are **not supported** — the implementation hooks into `ReactActivityDelegate` which lacks these methods.
```kotlin
class MyPackage : Package {
override fun createReactActivityLifecycleListeners(
activityContext: Context
): List<ReactActivityLifecycleListener> {
return listOf(MyActivityListener())
}
}
class MyActivityListener : ReactActivityLifecycleListener {
override fun onCreate(activity: Activity, savedInstanceState: Bundle?) { }
override fun onResume(activity: Activity) { }
override fun onPause(activity: Activity) { }
override fun onDestroy(activity: Activity) { }
override fun onNewIntent(intent: Intent?): Boolean { return false }
override fun onBackPressed(): Boolean { return false }
}
```
### ApplicationLifecycleListener
Supported callbacks: `onCreate`, `onConfigurationChanged`.
```kotlin
class MyPackage : Package {
override fun createApplicationLifecycleListeners(
context: Context
): List<ApplicationLifecycleListener> {
return listOf(MyAppListener())
}
}
class MyAppListener : ApplicationLifecycleListener {
override fun onCreate(application: Application) {
// App-level initialization
}
}
```
references/module-config.md
# Module Configuration Reference
## expo-module.config.json
Use `expo-module.config.json` for autolinking and module registration.
File placement depends on the module type:
- **standalone module**: place it at the package root, next to `package.json`
- **local module**: place it at the module root inside the app's local modules directory (`expo.autolinking.nativeModulesDir`, or `modules/` by default)
Example:
```json
{
"platforms": ["android", "apple", "web"],
"apple": {
"modules": ["MyModule"],
"appDelegateSubscribers": ["MyAppDelegateSubscriber"]
},
"android": {
"modules": ["expo.modules.mymodule.MyModule"]
}
}
```
### Fields
| Field | Description |
|-------|-------------|
| `platforms` | Array of supported platforms. Valid values include `android`, `apple`, `web`, and `devtools`. You can also use granular Apple platforms such as `ios`, `macos`, and `tvos`, but `apple` is preferred when one Swift module supports multiple Apple targets. |
| `apple.modules` | Swift module class names |
| `apple.appDelegateSubscribers` | Swift AppDelegate subscriber class names |
| `android.modules` | Fully-qualified Kotlin module class names (package + class) |
## Autolinking
Expo autolinking automatically discovers and links modules that have `expo-module.config.json`. No manual native project configuration needed — install via npm, run `pod install`.
- standalone modules are resolved from dependencies and search paths.
- local modules are resolved from the `modules` directory or `nativeModulesDir` if defined.
### Resolution Order
1. Explicit dependencies in `react-native.config.js`
2. Custom `searchPaths` directories
3. Local `nativeModulesDir` (defaults to `./modules/`)
4. Recursive npm dependency resolution
references/native-module.md
# Native Module DSL Reference
Swift is shown as the primary language. Kotlin follows the same DSL structure (see SKILL.md for both). Kotlin-specific syntax is noted where it meaningfully differs.
## Name
Sets the module identifier used in JavaScript.
```swift
Name("MyModule")
```
## Constant
Computed once on first access, then cached.
```swift
Constant("PI") { 3.14159 }
```
## Function (Synchronous)
Blocks the JS thread until completion. Supports up to 8 arguments.
```swift
Function("add") { (a: Int, b: Int) -> Int in
return a + b
}
```
## AsyncFunction
Returns a Promise. Runs on a background thread by default.
```swift
AsyncFunction("fetchData") { (url: URL) -> String in
let data = try Data(contentsOf: url)
return String(data: data, encoding: .utf8) ?? ""
}
// Force main queue execution
AsyncFunction("updateUI") { () -> Void in
// UI work
}.runOnQueue(.main)
```
**Kotlin differences:**
```kotlin
// Supports Kotlin coroutines
AsyncFunction("fetchData") Coroutine { url: java.net.URL ->
withContext(Dispatchers.IO) {
url.readText()
}
}
```
## Property
Getter/setter for JS object properties.
```swift
// Read-only
Property("version") { "1.0.0" }
// Read-write
Property("volume")
.get { () -> Float in self.volume }
.set { (newValue: Float) in self.volume = newValue }
```
## Events
Declares events the module can send to JS. Must be declared before using `sendEvent`.
```swift
// Declaration
Events("onChange", "onError")
// Sending from native (Swift)
sendEvent("onChange", ["value": newValue])
```
**Kotlin difference** — uses `bundleOf`:
```kotlin
sendEvent("onChange", bundleOf("value" to newValue))
```
**JS subscription:**
```typescript
import { useEvent } from "expo";
import MyModule from "./MyModule";
// Hook-based (recommended)
const event = useEvent(MyModule, "onChange");
// Manual subscription
const subscription = MyModule.addListener("onChange", (event) => {
console.log(event.value);
});
// Clean up: subscription.remove()
```
### OnStartObserving / OnStopObserving
Called when the first listener attaches / last listener detaches. Can be scoped to specific events.
```swift
OnStartObserving("onChange") {
// Start producing events
}
OnStopObserving("onChange") {
// Stop producing events
}
```
---
## Type System
### Primitives
| Swift | Kotlin | JS |
|-------|--------|----|
| `Bool` | `Boolean` | `boolean` |
| `Int`, `Int32` | `Int` | `number` |
| `Int64` | `Long` | `number` |
| `Float`, `Float32` | `Float` | `number` |
| `Double` | `Double` | `number` |
| `String` | `String` | `string` |
| `URL` | `java.net.URL` / `android.net.Uri` | `string` |
| `CGPoint` | - | `{ x, y }` |
| `CGSize` | - | `{ width, height }` |
| `CGRect` | - | `{ x, y, width, height }` |
| `UIColor` / `CGColor` | `android.graphics.Color` | `string` (ProcessedColorValue) |
| `Data` | `kotlin.ByteArray` | `Uint8Array` |
### Records (Struct-like types)
```swift
struct UserRecord: Record {
@Field var name: String = ""
@Field var age: Int = 0
@Field var email: String?
}
Function("createUser") { (user: UserRecord) -> Bool in
return true
}
```
**Kotlin difference** — uses `class` instead of `struct`, optional fields need explicit `= null`:
```kotlin
class UserRecord : Record {
@Field var name: String = ""
@Field var age: Int = 0
@Field var email: String? = null
}
```
### Enums (Enumerable)
```swift
enum Theme: String, Enumerable {
case light
case dark
case system
}
Function("setTheme") { (theme: Theme) in
// type-safe enum value
}
```
**Kotlin difference** — uses `enum class` with explicit `value` property:
```kotlin
enum class Theme(val value: String) : Enumerable {
LIGHT("light"),
DARK("dark"),
SYSTEM("system")
}
```
### Either Types (Union types)
```swift
Function("process") { (input: Either<String, Int>) in
if let str = input.get(String.self) {
// handle string
} else if let num = input.get(Int.self) {
// handle number
}
}
```
Also available: `EitherOfThree<A, B, C>`, `EitherOfFour<A, B, C, D>`.
### JavaScript Values (Direct JS manipulation)
For advanced use in synchronous functions running on JS thread:
```swift
Function("callback") { (fn: JavaScriptFunction<String>) in
let result = fn("arg1", "arg2")
}
```
---
## Shared Objects
Bridge native class instances to JS with automatic lifecycle management. Instances are deallocated when neither JS nor native code holds a reference.
### Defining a Shared Object
```swift
class ImageContext: SharedObject {
private var image: UIImage
init(image: UIImage) {
self.image = image
super.init()
}
func rotate(degrees: Double) {
image = image.rotated(degrees: degrees)
}
}
```
**Kotlin difference** — takes `RuntimeContext` in constructor, override `sharedObjectDidRelease()` for cleanup:
```kotlin
class ImageContext(
runtimeContext: RuntimeContext,
private var bitmap: Bitmap
) : SharedObject(runtimeContext) {
fun rotate(degrees: Float) { /* ... */ }
override fun sharedObjectDidRelease() {
if (!bitmap.isRecycled) bitmap.recycle()
}
}
```
### Exposing via Class DSL
```swift
Class("Context", ImageContext.self) {
Constructor { (path: String) -> ImageContext in
return ImageContext(image: UIImage(contentsOfFile: path)!)
}
Function("rotate") { (ctx: ImageContext, degrees: Double) -> ImageContext in
ctx.rotate(degrees: degrees)
return ctx
}
Property("width")
.get { (ctx: ImageContext) -> Int in ctx.width }
}
```
Other Class DSL components: `StaticFunction`, `StaticAsyncFunction`, `AsyncFunction`.
### SharedRef
Specialized shared reference for passing typed objects between modules:
```swift
final class ImageRef: SharedRef<UIImage> {}
```
### JS Usage
```typescript
const ctx = await ImageModule.create("/path/to/image.png");
ctx.rotate(90);
console.log(ctx.width);
```
references/native-view.md
# Native View Reference
Native views let you render platform UI components (UIView on iOS, Android View on Android) as React components.
## Defining a View
**Swift:**
```swift
public class MyViewModule: Module {
public func definition() -> ModuleDefinition {
Name("MyView")
View(MyNativeView.self) {
Prop("title") { (view: MyNativeView, title: String) in
view.titleLabel.text = title
}
Events("onPress", "onLoad")
AsyncFunction("reset") { (view: MyNativeView) in
view.reset()
}
}
}
}
class MyNativeView: ExpoView {
let titleLabel = UILabel()
required init(appContext: AppContext) {
super.init(appContext: appContext)
clipsToBounds = true
addSubview(titleLabel)
}
override func layoutSubviews() {
super.layoutSubviews()
titleLabel.frame = bounds
}
}
```
**Kotlin:**
```kotlin
class MyViewModule : Module() {
override fun definition() = ModuleDefinition {
Name("MyView")
View(MyNativeView::class) {
Prop("title") { view: MyNativeView, title: String ->
view.titleView.text = title
}
Events("onPress", "onLoad")
AsyncFunction("reset") { view: MyNativeView ->
view.reset()
}
}
}
}
class MyNativeView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
val titleView = TextView(context).also {
addView(it, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
}
}
```
**TypeScript:**
```typescript
import { requireNativeView } from "expo";
export type MyViewProps = {
title?: string;
onPress?: (event: { nativeEvent: { x: number; y: number } }) => void;
onLoad?: () => void;
} & ViewProps;
const NativeView = requireNativeView<MyViewProps>("MyView");
export function MyView(props: MyViewProps) {
return <NativeView {...props} />;
}
```
## View Event Dispatching
**Swift:**
```swift
class MyNativeView: ExpoView {
let onPress = EventDispatcher()
func handleTap(at point: CGPoint) {
onPress(["x": point.x, "y": point.y])
}
}
```
**Kotlin:**
```kotlin
class MyNativeView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
private val onPress by EventDispatcher()
fun handleTap(x: Float, y: Float) {
onPress(mapOf("x" to x, "y" to y))
}
}
```
## View Lifecycle
```swift
// Called after all props have been set
OnViewDidUpdateProps { (view: MyNativeView) in
view.applyChanges()
}
```
```kotlin
// Android only - called when view is no longer used
OnViewDestroys { view: MyNativeView ->
view.cleanup()
}
```
## AsyncFunction on Views
Functions defined inside `View` are accessible via React ref:
```typescript
const ref = useRef<MyView>(null);
// Call native function
await ref.current?.reset();
```
## PropGroup (Android)
Batch-register multiple props with shared setter logic:
```kotlin
View(MyNativeView::class) {
PropGroup("border", "width" to Float::class, "color" to Int::class) { view, index, value ->
when (index) {
0 -> view.borderWidth = value as Float
1 -> view.borderColor = value as Int
}
}
}
```
## GroupView (Android)
Enable view group functionality for managing child views:
```kotlin
View(MyContainerView::class) {
GroupView {
AddChildView { parent, child, index -> parent.addView(child, index) }
GetChildCount { parent -> parent.childCount }
GetChildViewAt { parent, index -> parent.getChildAt(index) }
RemoveChildView { parent, child -> parent.removeView(child) }
RemoveChildViewAt { parent, index -> parent.removeViewAt(index) }
}
}
```