Class definition DSL - StaticAsyncFunction
The StaticAsyncFunction component defines an asynchronous function on the class itself, callable from JavaScript as await ClassName.functionName(). Returns a Promise. On Kotlin, you can use the Coroutine modifier for suspendable bodies.
Swift syntax: StaticAsyncFunction("myStaticAsyncFunction") { in ... }
Kotlin syntax: StaticAsyncFunction("myStaticAsyncFunction") { -> ... }
Class definition DSL - Property
Inside a Class() block, Property receives the class instance as a parameter, similar to how Function does. This lets you expose computed properties on shared object instances. Properties can be read-only or read-write using .get and .set modifiers.
Swift read-only: Property("isPlaying") { (player: VideoPlayer) -> Bool in return player.isPlaying }
Swift read-write: Property("volume").get { (player: VideoPlayer) -> Float in return player.volume }.set { (player: VideoPlayer, volume: Float) in player.volume = volume }
Kotlin read-only: Property("isPlaying") { player: VideoPlayer -> return@Property player.isPlaying }
Kotlin read-write: Property("volume").get { player: VideoPlayer -> return@get player.volume }.set { player: VideoPlayer, volume: Float -> player.volume = volume }
JavaScript usage: const player = new VideoPlayer(source); console.log(player.isPlaying); player.volume = 0.5;
iOS Swift shared object example - image manipulation
```swift
import ExpoModulesCore
import UIKit
final class ImageRef: SharedRef<UIImage> {}
final class SimpleImageContext: SharedObject {
private var current: UIImage
init(path: String) throws {
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)),
let image = UIImage(data: data) else {
throw Exceptions.InvalidArgument()
}
self.current = image
super.init()
}
func rotate(by degrees: Double) {
current = current.rotated(degrees: degrees)
}
func flipX() {
current = current.withHorizontallyFlippedOrientation()
}
func render() -> ImageRef {
return ImageRef(current)
}
}
public final class SimpleImageModule: Module {
public func definition() -> ModuleDefinition {
Name("SimpleImageModule")
AsyncFunction("createContextAsync") { (path: String) -> SimpleImageContext in
return try SimpleImageContext(path: path)
}
Class("Context", SimpleImageContext.self) {
Function("rotate") { (ctx, degrees: Double) -> SimpleImageContext in
ctx.rotate(by: degrees)
return ctx
}
Function("flipX") { (ctx: SimpleImageContext) -> SimpleImageContext in
ctx.flipX()
return ctx
}
AsyncFunction("renderAsync") { (ctx: SimpleImageContext) -> ImageRef in
return ctx.render()
}
}
}
}
```
This example shows how to create a shared object that decodes an image from disk once, applies in-memory transforms like rotate and flipX, and exposes a render method that returns a SharedRef for consumption by other modules.
Android Kotlin shared object example - image manipulation
```kotlin
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.kotlin.sharedobjects.SharedObject
class ImageRef : SharedRef<Bitmap>()
class SimpleImageContext(
runtimeContext: RuntimeContext,
bitmap: Bitmap
) : SharedObject(runtimeContext) {
private var current: Bitmap = bitmap
fun rotate(degrees: Float) = apply {
val matrix = Matrix().apply { postRotate(degrees) }
current = Bitmap.createBitmap(current, 0, 0, current.width, current.height, matrix, true)
}
fun flipX() = apply {
val matrix = Matrix().apply { preScale(-1f, 1f) }
current = Bitmap.createBitmap(current, 0, 0, current.width, current.height, matrix, true)
}
fun render(): ImageRef = ImageRef(current, runtimeContext)
override fun sharedObjectDidRelease() {
if (!current.isRecycled) current.recycle()
}
}
class SimpleImageModule : Module() {
override fun definition() = ModuleDefinition {
Name("SimpleImageModule")
AsyncFunction("createContextAsync") { path: String ->
val bitmap = BitmapFactory.decodeFile(path)
?: throw Exceptions.IllegalArgument("Unable to decode image at $path")
SimpleImageContext(runtimeContext, bitmap)
}
Class<SimpleImageContext>("Context") {
Function("rotate") { ctx: SimpleImageContext, degrees: Float -> ctx.rotate(degrees) }
Function("flipX") { ctx: SimpleImageContext -> ctx.flipX() }
AsyncFunction("renderAsync") Coroutine { ctx: SimpleImageContext -> ctx.render() }
}
}
}
```
This example shows how to create a shared object that decodes an image from disk once, applies in-memory transforms like rotate and flipX, and exposes a render method that returns a SharedRef for consumption by other modules.
iOS Module definition for shared objects
In iOS, expose a shared object through the module definition using a declarative syntax with ModuleDefinition. Use AsyncFunction or Function to create instances and return the SharedObject. Use Class("ClassName", ClassName.self) block to bind methods to the shared object. Inside the Class block, use Function for synchronous methods and AsyncFunction for asynchronous methods. Example: AsyncFunction("createContextAsync") creates an instance, and Function("rotate") { (ctx, degrees: Double) -> ClassName in ... } binds instance methods.
Expo libraries using shared objects
Several Expo SDK libraries use shared objects: expo-image uses SharedObject to keep a decoded bitmap alive and accepts SharedRef<Bitmap> on Android and SharedRef<UIImage> on iOS; expo-image-manipulator demonstrates handling asynchronous operations, queuing multiple operations, and exposing a clean JavaScript API; expo-sqlite uses shared objects to keep database, session, and statement handles across calls while coordinating access to the underlying database; expo/fetch library uses shared objects to keep request and response lifecycles alive for streaming, cancellation, and redirect handling while presenting a JavaScript fetch-compatible API.
Class definition DSL - Constructor
The Constructor component defines a constructor that JavaScript code can use to create new instances of the shared object with new ClassName(args). Without a Constructor, instances can only be created by native functions that return the shared object. The constructor receives arguments from JavaScript and must return an instance of the shared object class.
Swift syntax: Constructor { (date: Date) in ... }
Kotlin syntax: Constructor { date: Date -> ... }
Android Module definition for shared objects
In Android, expose a shared object through the module definition using a declarative syntax with ModuleDefinition. Use AsyncFunction or Function to create instances and return the SharedObject. Use Class<ClassName>("ClassName") block to bind methods to the shared object. Inside the Class block, use Function for synchronous methods and AsyncFunction with Coroutine modifier for asynchronous methods. Example: AsyncFunction("createContextAsync") creates an instance, and Function("rotate") { ctx: ClassName, arg -> ctx.rotate(arg) } binds instance methods.
iOS shared object implementation with shared reference export
In iOS, when exposing a shared object result to other modules that understand specific reference types, create a specialized SharedRef subclass. For example, final class ImageRef: SharedRef<UIImage> {} creates a reference type that expo-image and other image-aware modules already understand. This allows the render method to return an ImageRef instead of the raw UIImage, enabling seamless integration with other Expo modules.
short-module-interface command
The short-module-interface command creates a short TypeScript interface for an Expo module. It overwrites ModuleName.generated.ts and creates ModuleName.ts if not present. It can be used with inline-modules and accepts standard Common CLI options.
expo-type-information package availability and requirements
The expo-type-information library works only on macOS and is available for SDK 56 and later. It provides tools to automatically generate TypeScript interfaces for Swift modules.
expo-type-information components
The expo-type-information package consists of four parts: a Swift parser based on sourcekitten that retrieves and structures type information from a Swift Expo module; a Type Abstraction layer that abstracts type information relevant for Expo modules; a TypeScript AST Emitter that provides functions for generating TypeScript code; and a CLI tool that integrates these functionalities.
Common CLI options for expo-type-information
Common CLI options used by most expo-type-information commands are: -i, --input-paths <filePaths...> (Paths to Swift files for some module, glob patterns are allowed); -m --module-path <modulePath> (Path to Expo module root directory); -o, --output-path <filePath> (Path to save the generated output, prints to console if not provided); -t, --type-inference <typeInference> (Level of type inference: NO_INFERENCE, SIMPLE_INFERENCE, or PREPROCESS_AND_INFERENCE, defaults to PREPROCESS_AND_INFERENCE); -s, --skip-unicode-character-mapping (Skip mapping all non-ASCII characters in a file to ASCII strings, by default this mapping is performed as SourceKitten is inconsistent when calculating offsets of non-ASCII characters); -w --watcher (Starts a watcher that checks for changes in input-path file).
module-interface command
The module-interface command generates a full TypeScript interface for a Swift module. It generates four files: types.ts with all types defined in the module, module.ts with the native module definition, view.tsx for each view defined in the module, and index.ts which reexports some functions. It accepts standard Common CLI options.
inline-modules-interface command
The inline-modules-interface command creates a TypeScript interface for every Swift inline module in the project. It generates Module.generated.ts which is regenerated with each run, and Module.tsx which is not regenerated if changed. Options include: -a --app-json <appJsonPath> (Path to app config file where inline.modules.watchedDirectories are defined); -w --watcher (Starts a watcher that checks for changes in inline modules files); -t, --type-inference <typeInference> (Level of type inference with values NO_INFERENCE, SIMPLE_INFERENCE, or PREPROCESS_AND_INFERENCE, defaults to SIMPLE_INFERENCE).
generate-mocks-for-file command
The generate-mocks-for-file command generates mocks for a given expo module and accepts standard Common CLI options.
Other expo-type-information commands
Internal or specific commands include: type-information (Parses Swift module type information and outputs a FileTypeInformation JSON); generate-module-types (Generates a type declaration file content for a module); generate-view-types (Generates a type declaration file for a native View); generate-jsx-intrinsics (Generates a declaration file for a View and updates JSX intrinsics with the View props); preprocess-file (Print the preprocessed file in the state right before parsing them using sourcekitten). All accept standard Common CLI options.
expo-type-information Swift parser limitations
Known limitations of expo-type-information: Nested classes will not be resolved fully due to sourcekitten limits on resolving nested closures; Return type resolution with PREPROCESS_AND_INFERENCE option sometimes fails due to file rewriting issues with strings and comments, and cannot resolve tail expressions; Not all DSL declarations are parsed in every context, for example Events are parsed inside a View but not in Module definitions; Using Unicode characters breaks sourcekitten offsets.
Supported Expo modules DSL declarations and Swift features
Supported features in expo-type-information: Expo DSL Declarations support parsing for AsyncFunction, Constant, Constructor, Events, Function, Name, Prop, Property, and View; Swift struct and class must conform to the Record protocol and only properties marked with the @Field annotation are parsed; Swift enum basic cases are supported but values associated with enum cases are not currently parsed.
Type inference option recommendation for PREPROCESS_AND_INFERENCE failures
If the PREPROCESS_AND_INFERENCE type inference option encounters errors, fall back to SIMPLE_INFERENCE or NO_INFERENCE.
Expo modules implementation languages by platform
Expo modules are implemented in the following languages by platform: expo-battery (Swift only), expo-cellular (Kotlin and Swift), expo-clipboard (Kotlin and Swift), expo-crypto (Kotlin and Swift), expo-device (Swift only), expo-haptics (Swift only), expo-image-manipulator (Swift only), expo-image-picker (Kotlin and Swift), expo-linear-gradient (Kotlin and Swift), expo-localization (Kotlin and Swift), expo-store-review (Swift only), expo-system-ui (Swift only), expo-video-thumbnails (Swift only), and expo-web-browser (Kotlin and Swift).
Test published Expo module in new project
To test a published Expo module, create a new app using create-expo-app with a template (e.g., default@sdk-57), navigate to the app directory, run expo install with the module name (e.g., expo install expo-settings), then run expo prebuild --clean and expo run:android or expo run:ios.
Monorepo directory structure for Expo modules
A monorepo for Expo modules should have three main directories: apps (stores multiple projects, including React Native apps), packages (keeps different packages used by the apps), and a root package.json (contains Yarn workspaces configuration).
Create Expo module with --no-example flag in monorepo
When setting up a module in a monorepo, use create-expo-module with the --no-example flag to skip creating the example app. Example: npx create-expo-module packages/expo-settings --no-example
Add native module to monorepo app dependencies
To use a native module from the packages directory in an app, add it to that app's package.json dependencies with version "*". Example: "expo-settings": "*" in the app's dependencies.
Build TypeScript compiler for monorepo module
In a monorepo setup, navigate to the module directory (packages/expo-settings) and run npm run build (or yarn/pnpm/bun equivalent) to start the TypeScript compiler that watches for changes and rebuilds the module's JavaScript.
Prebuild and run monorepo app with new module
After setting up a module in a monorepo, run expo prebuild --clean in the app directory, then use expo run:android or expo run:ios to compile and run the app with the new module.
Alternatives to npm for publishing Expo modules
Instead of publishing to npm directly, you can use npm pack to create a tarball for local testing, run a local npm registry using tools like Verdaccio, or use a private registry with EAS Build for private packages.
Publishing Expo module to npm requires npm account
Before publishing an Expo module to npm, create an npm account at https://www.npmjs.com/signup, then log in using npm login, and finally run npm publish from the module's root directory.
Telemetry and opt-out
Expo dev tools collect anonymous usage data to identify issues. Telemetry is optional and can be disabled with `EXPO_NO_TELEMETRY=1` environment variable.
Metro setup for existing React Native apps
Projects not using Expo Prebuild need additional setup to ensure custom Expo bundling features work. See Metro setup for existing React Native apps documentation.
Expo CLI main entry point and alias
The Expo CLI is accessed via `npx expo` or `npx expo start`, which provides a command-line interface for managing Expo projects. `npx expo` can be used as an alias for `npx expo start` to launch the development server.
Expo CLI core commands
The main Expo CLI commands are: start (develop), export, run:ios, run:android, prebuild, install, customize, config, login, logout, whoami, register. Each command can be invoked with `--help` or `-h` flags to learn more about options.
Development server URL default port
`npx expo start` launches a development server on `http://localhost:8081` that provides a Metro bundler interface with a QR code and keyboard shortcuts in the Terminal UI.
Terminal UI keyboard shortcuts
The Expo development Terminal UI supports the following keyboard shortcuts: A (open on Android device), Shift+A (select Android device), I (open iOS Simulator), Shift+I (select iOS Simulator), W (open web browser), R (reload app), S (switch between Expo Go and development builds), M (open dev menu), Shift+M (choose more device commands), J (open React Native DevTools for Hermes), O (open project in editor), E (show QR code), ? (show all commands).
Launch target selection during development
`npx expo start` automatically launches in a development build if `expo-dev-client` is installed; otherwise it launches in Expo Go. Use `--dev-client` flag to force development build, `--go` flag to force Expo Go. Press S in Terminal UI to switch runtime at any time.
Development server network options
By default `npx expo start` serves over LAN. Use `--localhost` flag for localhost-only connection. The `--port` flag (default 8081) sets the dev server port; use `--port 0` for automatic port selection. The `--https` flag (deprecated in favor of `--tunnel`) enables secure origin on web.
EXPO_PACKAGER_PROXY_URL environment variable
Use `EXPO_PACKAGER_PROXY_URL` environment variable to force the dev server URL to a specific value. Example: `export EXPO_PACKAGER_PROXY_URL=http://expo.dev` opens apps to `exp://expo.dev:80`.
Tunnel mode with ngrok for restricted networks
For restrictive networks or firewalls, enable tunneling by installing `@expo/ngrok` globally, then run `npx expo start --tunnel` to serve from a public URL like `https://xxxxxxx.bacon.19000.exp.direct:80`. Use `EXPO_TUNNEL_SUBDOMAIN` environment variable to set the subdomain experimentally.
Tunnel mode drawbacks
Tunneling is slower than local connections due to forwarding to public URLs. Tunnel URLs are public and accessible by any device with network; Expo CLI mitigates exposure by adding entropy to the URL which can be reset by clearing the .expo directory. Tunnels require network connection on both devices and cannot be used with `--offline` flag. Tunneling may experience intermittent issues from ngrok outages.
Offline development mode
Use `npx expo start --offline` to develop without a network connection. Offline mode prevents CLI from making network requests. If not flagged, offline support automatically enables if the computer has no internet connection, though verification takes longer. Expo CLI signs manifests with user credentials for secure OTA usage in online mode.
.expo directory structure
When starting the dev server for the first time, a .expo directory is created at the project root containing: devices.json (recent device information) and settings.json (server configuration). These files are local-specific and should be in .gitignore; they are not meant to be shared with other developers.
Open endpoint for dev server discovery
The dev server exposes `/_expo/open` endpoint for external tools to introspect deep links and trigger app launches. GET returns deep link as JSON (dry run, safe for tunnels). POST opens project locally on requested platform (iOS Simulator, Android emulator, or web browser); restricted to same-origin requests.
Open endpoint query parameters
`/_expo/open` endpoint accepts query parameters: `platform` (ios, android, or web; omit on GET for discovery), and `runtime` (default: mirrors I/A keys, expo: force Expo Go, custom: force dev-build, unknown: force disambiguation page). Omitting platform on GET returns discovery response keyed by platform.
Open endpoint GET response structure
GET `/_expo/open` with platform returns: runtime (resolved runtime: expo, custom, or web), url (deep link), scheme (project URL scheme or null), availableRuntimes (array of expo and/or custom), appId (iOS bundle identifier, Android package name, or null). Without platform, response is keyed by platform showing url, appId per platform, and scheme/availableRuntimes at top level.
Open endpoint POST response codes
POST `/_expo/open` returns: 200 with { platform, runtime, url } describing what was opened; 403 for cross-origin POST with error explaining host mismatch; 501 when host cannot launch requested platform with details field; 500 when openPlatformAsync throws with error code and message.
npx expo run:ios and run:android local compilation
Local compilation commands: `npx expo run:ios` (Mac only, requires Xcode) and `npx expo run:android` (requires Android Studio and Java). These build directly on connected devices with no global side effects, support locked devices, automatically codesign iOS apps for development, show smart log parsing, and surface fatal errors in terminal.
Cross-platform build arguments for run commands
Common arguments for `npx expo run:ios` and `npx expo run:android`: --no-build-cache (clear native cache), --no-install (skip dependency installation), --no-bundler (skip dev server), -d/--device [name/ID] (target device; omit to select from list; use generic to build without targeting), -o/--output <path> (copy built app to directory), -p/--port <port> (dev server port, default 8081), --binary <path> (install existing binary instead of building).
Android build variants: debug
Android debug variant built with `npx expo run:android --variant debug` for standard debug builds.
Android build variants: debugOptimized
`debugOptimized` variant (SDK 54+) enables faster development with performance near release builds via `npx expo run:android --variant debugOptimized`. Optimizes C++ libraries like release builds. In EAS Build, use matching Gradle command like `:app:assembleDebugOptimized` in eas.json. Limitation: C++ debugging disabled, C++ crashes have less readable stack traces.
Android build variants: release
`npx expo run:android --variant release` compiles for production but does not auto code-sign for Play Store. Use for testing production-only bugs. For production Play Store builds, use EAS Build.
Android build with custom product flavors
For customized Android projects with product flavors, use `npx expo run:android --variant freeDebug --app-id dev.expo.myapp.free` to configure both variant and application ID.
iOS build schemes and sub-apps
`npx expo run:ios` selects the default iOS app scheme. Use `--scheme <my-scheme>` to pick a custom scheme for different sub-apps like App Clips, watchOS apps, or Safari Extensions. Omitting the scheme value prompts selection from available options. Selected scheme filters available --device options.
iOS production build compilation
Compile iOS app for production with `npx expo run:ios --configuration Release`. This build is not auto code-signed for App Store. Use for testing production-only bugs. For production App Store builds with code signing, use EAS Build.
iOS build-only workflow with generic device
`npx expo run:ios --device generic` builds a Simulator app without targeting a specific device, using generic Xcode destination (`generic/platform=iOS Simulator`). Useful for CI/CD pipelines, distributing .app bundles, and build-only workflows. CLI outputs path to built .app bundle. Combine with `--configuration Release --output ./build` for production simulator builds.
iOS development signing with connected device
Connect an iOS device and run `npx expo run:ios --device` to select it. Expo CLI automatically signs the device for development, installs the app, and launches it. If no developer profiles exist, manually set them up per the Setup Xcode signing guide.
npx expo export for production bundling
`npx expo export` bundles JavaScript and assets for production using Metro bundler. It transpiles code, strips `__DEV__` boolean, copies static files to **dist** directory, and copies **public** directory contents to **dist** as-is. Works like typical web frameworks.
npx expo export platform and output options
`npx expo export` options: --platform <platform> (ios, android, all, or web; default all), --dev (bundle for development without minifying or stripping __DEV__), --output-dir <dir> (export directory; default dist), --max-workers <number> (bundler tasks; 0 runs transpilation on same process), -c/--clear (clear bundler cache), --no-minify (skip minifying JS/CSS), --no-bytecode (skip Hermes bytecode for native), --no-ssg (skip generating HTML for web routes, useful for API routes).
Hosting with baseUrl sub-paths configuration
Configure sub-path hosting (experimental) by setting `experiments.baseUrl` in app.json. Example: `{ "expo": { "experiments": { "baseUrl": "/my-root" } } }` exports website with all resources prefixed with `/my-root`. Expo Router automatically prepends baseUrl to Link and router APIs.
baseUrl export behavior with Link component
When using Expo Router with baseUrl, `<Link href="/blog/123">Go to blog post</Link>` exports to `<a href="/my-root/blog/123">Go to blog post</a>`. baseUrl is automatically prepended to Link and router APIs but must be manually prepended when using `<a>`, React Navigation, or Linking API directly.