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
82 changes: 82 additions & 0 deletions docs/pages/more/expo-cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,88 @@ When you start the development server in a project for the first time, a **.expo

Both of these files have information that is specific to your local computer. This is the reason why **.expo** directory is included in the **.gitignore** file, by default, when a new project is created. It is not meant to be shared with other developers.

### Open endpoint

The dev server exposes `/_expo/open` so external tools, such as cloud agents, remote preview services, CI scripts, can introspect the deep links the CLI would use, and optionally trigger the same action as pressing <kbd>I</kbd> / <kbd>A</kbd> / <kbd>W</kbd> in the **Terminal UI**. It supplements the legacy `/_expo/link` endpoint, which returns a `307` redirect to a deep link scheme that non-mobile clients can't follow.

| Method | Effect |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET` | Dry run: returns the deep link as JSON. Safe to call across tunnels. |
| `POST` | Opens the project locally — equivalent to pressing <kbd>I</kbd> / <kbd>A</kbd> / <kbd>W</kbd> in the **Terminal UI**. Restricted to same-origin requests. |

#### Query params

- `platform` (or `expo-platform` header): `ios`, `android`, or `web`. Omit on `GET` for a discovery response that lists every platform.
- `runtime`: choose how the URL is resolved.
- `default` (omitted): mirrors what pressing <kbd>I</kbd> / <kbd>A</kbd> does. It picks the dev client when the server was started with `--dev-client`, falls back to a disambiguation page when the project has both Expo Go and a development build, otherwise opens Expo Go.
- `expo`: force the Expo Go deep link (`exp://…`).
- `custom`: force the development-build deep link (`<scheme>://expo-development-client/?url=…`).
- `unknown`: force the disambiguation `/_expo/loading` page, letting the device decide between Expo Go and the dev build.

The endpoint reflects mid-run state changes — pressing <kbd>S</kbd> to toggle between Expo Go and the dev client, or installing `expo-dev-client` while the server is running, both show up on the next request.

#### GET response

For a specific platform:

```json
{
"runtime": "expo",
"url": "exp://192.168.1.71:8081",
"scheme": "myapp",
"availableRuntimes": ["expo", "custom"],
"appId": "com.example.app"
}
```

- `runtime`: the resolved runtime (`expo`, `custom`, or `web`). Omitted when `url` is the disambiguation page; the device determines the eventual runtime.
- `url`: the deep link (or disambiguation page URL for `runtime: 'unknown'` and the `default`-with-both-runtimes case). Routes through the ngrok host when `--tunnel` is active.
- `scheme`: the project's URL scheme used for development-build deep links, or `null` when none is configured.
- `availableRuntimes`: `['expo']`, `['custom']`, or `['expo', 'custom']`. When `.length > 1`, the caller should either pick a runtime explicitly or let the device disambiguate.
- `appId`: the iOS bundle identifier or Android package name resolved from the project config (or native files). `null` for web, or when the project hasn't set `ios.bundleIdentifier` / `android.package`. Useful for verifying a build is installed on a remote device before opening the deep link.

Without `platform`, the response is keyed by platform for discovery:

```json
{
"scheme": "myapp",
"availableRuntimes": ["expo", "custom"],
"platforms": {
"ios": {
"url": "http://192.168.1.71:8081/_expo/loading?platform=ios",
"appId": "com.example.app"
},
"android": {
"url": "http://192.168.1.71:8081/_expo/loading?platform=android",
"appId": "com.example.app"
},
"web": { "runtime": "web", "url": "http://192.168.1.71:8081", "appId": null }
}
}
```

#### POST behavior

`POST /_expo/open?platform=ios` opens the project locally on the requested platform (`ios` → iOS Simulator, `android` → Android emulator, `web` → desktop browser). Responses:

- `200`: `{ "platform", "runtime", "url" }` describing what was opened.
- `403`: cross-origin POST. The body's `error` explains the host mismatch and points at `GET /_expo/open` as the safe alternative.
- `501`: host can't launch the requested platform (for example, `platform=ios` on Linux/Windows). The response carries a `details` field explaining why and suggesting the GET-then-launch-remotely workflow.
- `500`: `openPlatformAsync` threw. The body forwards the underlying error code and message.

#### Examples

```sh
# Get the deep link for iOS (works over a tunnel, no Expo Go install required).
curl http://localhost:8081/_expo/open?platform=ios

# Force the disambiguation page so a device or external picker chooses.
curl 'http://localhost:8081/_expo/open?platform=android&runtime=unknown'

# Trigger an iOS Simulator launch (only works on the dev server's host).
curl -X POST http://localhost:8081/_expo/open?platform=ios
```

## Building

A React Native app consists of two parts: a native runtime ([compiling](#compiling)), and static files like JavaScript bundles and assets ([exporting](#exporting)). Expo CLI provides commands for performing both tasks.
Expand Down
2 changes: 2 additions & 0 deletions packages/@expo/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

### 🎉 New features

- Add `/_expo/open` middleware for programmatically resolving deep links and disambiguation pages for the running dev server. ([#45804](https://github.com/expo/expo/pull/45804) by [@EvanBacon](https://github.com/EvanBacon))

### 🐛 Bug fixes

### 💡 Others
Expand Down
5 changes: 5 additions & 0 deletions packages/@expo/cli/src/start/server/UrlCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ export class UrlCreator {
return this.gatewayInfo.address;
}

/** URL scheme configured for development-build deep links (e.g. `myapp`). `null` when unset. */
public getScheme(): string | null {
return this.defaults?.scheme ?? null;
}

/** Get the URL components from the Ngrok server URL. */
private getTunnelUrlComponents(options: Pick<CreateURLOptions, 'scheme'>): UrlComponents | null {
const tunnelUrl = this.bundlerInfo.getTunnelUrl?.();
Expand Down
47 changes: 47 additions & 0 deletions packages/@expo/cli/src/start/server/metro/MetroBundlerDevServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ import { CommandError } from '../../../utils/errors';
import { toPosixPath } from '../../../utils/filePath';
import { getEnvFiles, reloadEnvFiles } from '../../../utils/nodeEnv';
import { getFreePortAsync } from '../../../utils/port';
import { AndroidAppIdResolver } from '../../platforms/android/AndroidAppIdResolver';
import { AppleAppIdResolver } from '../../platforms/ios/AppleAppIdResolver';
import type { BundlerStartOptions, DevServerInstance } from '../BundlerDevServer';
import { BundlerDevServer } from '../BundlerDevServer';
import { evalMetroAndWrapFunctions, evalMetroNoHandling } from '../getStaticRenderFunctions';
Expand All @@ -83,6 +85,7 @@ import { createDomComponentsMiddleware } from '../middleware/DomComponentsMiddle
import { FaviconMiddleware } from '../middleware/FaviconMiddleware';
import { HistoryFallbackMiddleware } from '../middleware/HistoryFallbackMiddleware';
import { InterstitialPageMiddleware } from '../middleware/InterstitialPageMiddleware';
import { OpenHostSupportEntry, OpenMiddleware, OpenPlatform } from '../middleware/OpenMiddleware';
import { RuntimeRedirectMiddleware } from '../middleware/RuntimeRedirectMiddleware';
import { ServeStaticMiddleware } from '../middleware/ServeStaticMiddleware';
import type { ExpoMetroOptions } from '../middleware/metroOptions';
Expand All @@ -95,6 +98,7 @@ import {
getMetroDirectBundleOptions,
} from '../middleware/metroOptions';
import { prependMiddleware } from '../middleware/mutations';
import { createInfoHandler, createOpen } from '../middleware/openHandlers';
import type { ServerNext, ServerRequest, ServerResponse } from '../middleware/server.types';
import { startTypescriptTypeGenerationAsync } from '../type-generation/startTypescriptTypeGeneration';

Expand Down Expand Up @@ -1378,6 +1382,49 @@ export class MetroBundlerDevServer extends BundlerDevServer {
});
middleware.use(deepLinkMiddleware.getHandler());

const getHostSupport = (platform: OpenPlatform): OpenHostSupportEntry => {
if (platform === 'ios' && process.platform !== 'darwin') {
return {
canOpen: false,
reason: `iOS simulators require macOS with Xcode installed; this dev server is running on ${process.platform}.`,
};
}
return { canOpen: true };
};

// Read all dev-server state live — pressing `s` in the terminal toggles `isDevClient`
// and the scheme, and `expo-dev-client` can be installed mid-run (re-resolved by
// isRedirectPageEnabled on every call).
const openMiddleware = new OpenMiddleware(this.projectRoot, {
serverBaseUrl,
getHostSupport,
getInfo: createInfoHandler({
urlCreator: this.getUrlCreator(),
getIsDevClient: () => this.isDevClient,
getIsRedirectPageEnabled: () => this.isRedirectPageEnabled(),
getAppId: async (platform) => {
if (platform === 'web') return null;
const resolver =
platform === 'ios'
? new AppleAppIdResolver(this.projectRoot)
: new AndroidAppIdResolver(this.projectRoot);
try {
return await resolver.getAppIdAsync();
} catch {
// Surfacing the error would block the dry-run; consumers can detect the missing id
// from `appId: null` and prompt the user to configure ios.bundleIdentifier /
// android.package.
return null;
}
},
}),
open: createOpen({
getIsDevClient: () => this.isDevClient,
openPlatformAsync: (target, resolver) => this.openPlatformAsync(target, resolver),
}),
});
middleware.use(openMiddleware.getHandler());

const domComponentRenderer = createDomComponentsMiddleware(
{ projectRoot: this.projectRoot },
instanceMetroOptions
Expand Down
Loading
Loading