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
2 changes: 2 additions & 0 deletions packages/@expo/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

- Add missing `Content-Type: application/json` to internal `/symbolicate` call ([#43074](https://github.com/expo/expo/pull/43074) by [@kitten](https://github.com/kitten))
- Key loader data by `contextKey` instead of URL pathname ([#43017](https://github.com/expo/expo/pull/43017) by [@hassankhan]
- Fix port prompt causing process hangs on some systems when checking the conflicting process's info ([#43054](https://github.com/expo/expo/pull/43054) by [@kitten](https://github.com/kitten))

### 💡 Others

Expand All @@ -19,6 +20,7 @@
- Drop obsolete `rawBody` parsing from Metro middleware stack ([#43074](https://github.com/expo/expo/pull/43074) by [@kitten](https://github.com/kitten))
- Add minimum Node.js version check and warning to CLI startup ([#43076](https://github.com/expo/expo/pull/43076) by [@kitten](https://github.com/kitten))
- Retrieve default route's IP address concurrently ([#42923](https://github.com/expo/expo/pull/42923) by [@kitten](https://github.com/kitten))
- Replace `require-from-string` with `@expo/require-utils` ([#42884](https://github.com/expo/expo/pull/42884) by [@kitten](https://github.com/kitten))

## 55.0.7 — 2026-02-08

Expand Down
12 changes: 6 additions & 6 deletions packages/@expo/cli/e2e/__tests__/customize-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ it('runs `npx expo customize tsconfig.json`', async () => {
'with-router',
{
reuseExisting: false,
// TODO(@hassankhan): remove @expo/router-server after publishing
linkExpoPackages: ['expo-router', '@expo/router-server'],
// TODO(@hassankhan): remove @expo/router-server & require-utils after publishing
linkExpoPackages: ['expo-router', '@expo/router-server', '@expo/require-utils'],
}
);

Expand All @@ -111,8 +111,8 @@ it('runs `npx expo customize tsconfig.json` on a partially setup project', async
'with-router',
{
reuseExisting: false,
// TODO(@hassankhan): remove expo-router and @expo/router-server after publishing
linkExpoPackages: ['expo-router', '@expo/router-server'],
// TODO(@hassankhan): remove @expo/router-server & require-utils after publishing
linkExpoPackages: ['expo-router', '@expo/router-server', '@expo/require-utils'],
}
);

Expand Down Expand Up @@ -146,10 +146,10 @@ it('runs `npx expo customize tsconfig.json` sets up typed routes', async () => {
const projectRoot = await setupTestProjectWithOptionsAsync(
'expo-customize-typed-routes',
'with-router-typed-routes',
// TODO(@hassankhan): remove @expo/router-server after publishing
{
reuseExisting: false,
linkExpoPackages: ['expo-router', '@expo/log-box', '@expo/router-server'],
// TODO(@hassankhan): remove @expo/router-server & require-utils after publishing
linkExpoPackages: ['expo-router', '@expo/log-box', '@expo/router-server', '@expo/require-utils'],
}
);

Expand Down
2 changes: 1 addition & 1 deletion packages/@expo/cli/e2e/__tests__/export-monorepo-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ beforeAll(async () => {
// NOTE(cedric): this is a temporary workaround to avoid `@expo/cli` or `@expo/metro-config` to link to packages inside the expo/expo monorepo
// For some reason, this has gotten more unstable than it was and may result in unexpected SHA1 files not being calculated (even though they are included in the watch folders)
// TODO(@hassankhan): remove expo-router and @expo/router-server after publishing
linkExpoPackages: ['@expo/cli', '@expo/router-server', 'expo-router'],
linkExpoPackages: ['@expo/cli', '@expo/router-server', 'expo-router', '@expo/require-utils'],
});
});

Expand Down
4 changes: 2 additions & 2 deletions packages/@expo/cli/e2e/__tests__/install-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,8 @@ describe('expo-router integration', () => {
'with-router',
{
reuseExisting: false,
// TODO(@hassankhan): remove @expo/router-server after publishing
linkExpoPackages: ['expo-router', '@expo/router-server'],
// TODO(@hassankhan): remove @expo/router-server & require-utils after publishing
linkExpoPackages: ['expo-router', '@expo/router-server', '@expo/require-utils'],
}
);

Expand Down
2 changes: 1 addition & 1 deletion packages/@expo/cli/e2e/__tests__/metro-server-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const expo = createExpoStart({

beforeAll(async () => {
expo.options.cwd = await setupTestProjectWithOptionsAsync('metro-server', 'with-assets', {
linkExpoPackages: ['expo', '@expo/log-box', '@expo/local-build-cache-provider'],
linkExpoPackages: ['expo', '@expo/log-box', '@expo/local-build-cache-provider', '@expo/require-utils'],
});
await expo.startAsync();
});
Expand Down
3 changes: 2 additions & 1 deletion packages/@expo/cli/e2e/__tests__/start-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,11 @@ describeSkipWin('server', () => {

beforeEach(async () => {
expo.options.cwd = await setupTestProjectWithOptionsAsync('basic-start', 'with-blank', {
// TODO(@hassankhan, @krystofwoldrich): remove all linked after publishing
// TODO(@hassankhan, @krystofwoldrich, @kitten): remove all linked after publishing
linkExpoPackages: [
'@expo/router-server',
'@expo/log-box',
'@expo/require-utils',
'expo',
'@expo/local-build-cache-provider',
],
Expand Down
1 change: 1 addition & 0 deletions packages/@expo/cli/e2e/playwright/dev/hmr-env-vars.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ test.describe('router-e2e with spaces', () => {
'babel-preset-expo',
'@expo/metro-config',
'expo-server',
'@expo/require-utils',
],
}
);
Expand Down
2 changes: 1 addition & 1 deletion packages/@expo/cli/e2e/playwright/dev/with-spaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ test.describe('router-e2e with spaces', () => {
{
// TODO(@hassankhan, @krystofwoldrich): remove linked packages after publishing
linkExpoPackages: ['@expo/router-server', 'expo-router', '@expo/log-box'],
linkExpoPackagesDev: ['@expo/cli', 'expo-server'],
linkExpoPackagesDev: ['@expo/cli', 'expo-server', '@expo/require-utils'],
}
);

Expand Down
2 changes: 1 addition & 1 deletion packages/@expo/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@expo/package-manager": "^1.10.3",
"@expo/plist": "^0.5.2",
"@expo/prebuild-config": "^55.0.4",
"@expo/require-utils": "^55.0.0",
"@expo/router-server": "^55.0.5",
"@expo/schema-utils": "^55.0.2",
"@expo/spawn-async": "^1.7.2",
Expand Down Expand Up @@ -86,7 +87,6 @@
"pretty-format": "^29.7.0",
"progress": "^2.0.3",
"prompts": "^2.3.2",
"require-from-string": "^2.0.2",
"resolve-from": "^5.0.0",
"semver": "^7.6.0",
"send": "^0.19.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
import { getMetroServerRoot } from '@expo/config/paths';
import { evalModule } from '@expo/require-utils';
import fs from 'fs';
import path from 'path';
import requireString from 'require-from-string';

import { IS_METRO_BUNDLE_ERROR_SYMBOL, logMetroError } from './metro/metroErrorInterface';
import { createBundleUrlPath, ExpoMetroOptions } from './middleware/metroOptions';
Expand Down Expand Up @@ -147,5 +147,5 @@ export function evalMetroNoHandling(projectRoot: string, src: string, filename:
debug(`evalMetroNoHandling received filename outside of the project root: ${filename}`);
}

return profile(requireString, 'eval-metro-bundle')(src, filename);
return profile(evalModule, 'eval-metro-bundle')(src, filename);
}
16 changes: 9 additions & 7 deletions packages/@expo/cli/src/start/server/metro/runServer-fork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import type { WebSocketServer } from 'ws';

import { MetroBundlerDevServer } from './MetroBundlerDevServer';
import { Log } from '../../../log';
import { getRunningProcess } from '../../../utils/getRunningProcess';
import type { ConnectAppType } from '../middleware/server.types';

export const runServer = async (
Expand Down Expand Up @@ -79,12 +78,15 @@ export const runServer = async (
if ('code' in error && error.code === 'EADDRINUSE') {
// If `Error: listen EADDRINUSE: address already in use :::8081` then print additional info
// about the process before throwing.
const info = getRunningProcess(config.server.port);
if (info) {
Log.error(
`Port ${config.server.port} is busy running ${info.command} in: ${info.directory}`
);
}
const { getRunningProcess } =
require('../../../utils/getRunningProcess') as typeof import('../../../utils/getRunningProcess');
getRunningProcess(config.server.port).then((info) => {
if (info) {
Log.error(
`Port ${config.server.port} is busy running ${info.command} in: ${info.directory}`
);
}
});
}

if (onError) {
Expand Down
28 changes: 19 additions & 9 deletions packages/@expo/cli/src/utils/__tests__/getRunningProcess-test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import { execFileSync, execSync } from 'child_process';
import spawnAsync from '@expo/spawn-async';

import { getDirectoryOfProcessById, getPID } from '../getRunningProcess';
import { getDirectoryOfProcessById, getPID, getProcessCommand } from '../getRunningProcess';

jest.mock('@expo/spawn-async');

describe(getPID, () => {
it(`should return the pid value for a running port`, () => {
jest.mocked(execFileSync).mockImplementationOnce(() => '63828');
const pid = getPID(63828);
it(`should return the pid value for a running port`, async () => {
jest.mocked(spawnAsync).mockResolvedValueOnce({ stdout: ' 63828 ' } as any);
const pid = await getPID(8081);
expect(pid).toBe(63828);
});
});

describe(getDirectoryOfProcessById, () => {
it(`should return the directory of a pid`, () => {
jest.mocked(execSync).mockImplementationOnce(() => 'cwd');
const directory = getDirectoryOfProcessById(63828);
expect(directory).toBe('cwd');
it(`should return the directory of a pid`, async () => {
jest.mocked(spawnAsync).mockResolvedValueOnce({ stdout: '\nn/test/folder\n' } as any);
const directory = await getDirectoryOfProcessById(63828);
expect(directory).toBe('/test/folder');
});
});

describe(getProcessCommand, () => {
it(`should return the argv of a pid`, async () => {
jest.mocked(spawnAsync).mockResolvedValueOnce({ stdout: 'command arg' } as any);
const command = await getProcessCommand(63828, __dirname);
expect(command).toBe('command arg');
});
});
100 changes: 62 additions & 38 deletions packages/@expo/cli/src/utils/getRunningProcess.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import { execFileSync, execSync, ExecSyncOptionsWithStringEncoding } from 'child_process';
import spawnAsync from '@expo/spawn-async';
import * as path from 'path';

const debug = require('debug')('expo:utils:getRunningProcess') as typeof console.log;

const defaultOptions: ExecSyncOptionsWithStringEncoding = {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
};
/** Timeout applied to shell commands */
const timeout = 350;

/** Returns a pid value for a running port like `63828` or null if nothing is running on the given port. */
export function getPID(port: number): number | null {
export async function getPID(port: number): Promise<number | null> {
try {
const results = execFileSync('lsof', [`-i:${port}`, '-P', '-t', '-sTCP:LISTEN'], defaultOptions)
.split('\n')[0]
.trim();
const pid = Number(results);
const { stdout } = await spawnAsync('lsof', [`-i:${port}`, '-P', '-t', '-sTCP:LISTEN'], {
timeout,
});
const pid = Number(stdout.split('\n', 1)[0].trim());
debug(`pid: ${pid} for port: ${port}`);
return pid;
return Number.isSafeInteger(pid) ? pid : null;
} catch (error: any) {
debug(`No pid found for port: ${port}. Error: ${error}`);
return null;
Expand All @@ -25,55 +23,81 @@ export function getPID(port: number): number | null {

/** Get `package.json` `name` field for a given directory. Returns `null` if none exist. */
function getPackageName(packageRoot: string): string | null {
const packageJson = path.join(packageRoot, 'package.json');
try {
const packageJson = path.resolve(packageRoot, 'package.json');
return require(packageJson).name || null;
} catch {
} catch (error) {
return null;
}
}

/** Returns a command like `node /Users/evanbacon/.../bin/expo start` or the package.json name. */
function getProcessCommand(pid: number, procDirectory: string): string {
const name = getPackageName(procDirectory);

if (name) {
return name;
export async function getProcessCommand(
pid: number,
procDirectory: string
): Promise<string | null> {
let name = getPackageName(procDirectory);
if (!name) {
// ps
// -o args=: Output argv without header
// -p [pid]: For process of PID
const { stdout } = await spawnAsync('ps', ['-o', 'args=', '-p', `${pid}`], {
timeout,
});
name = stdout.trim();
}
return execSync(`ps -o command -p ${pid} | sed -n 2p`, defaultOptions).replace(/\n$/, '').trim();
return name || null;
}

/** Get directory for a given process ID. */
export function getDirectoryOfProcessById(processId: number): string {
return execSync(
`lsof -p ${processId} | awk '$4=="cwd" {for (i=9; i<=NF; i++) printf "%s ", $i}'`,
defaultOptions
).trim();
export async function getDirectoryOfProcessById(pid: number): Promise<string | null> {
try {
// lsof
// -F n: ask for machine readable output
// -a: apply conditions as logical AND
// -d cwd: Filter by cwd fd
// -p [pid]: Filter by input process id
const { stdout } = await spawnAsync('lsof', ['-F', 'n', '-a', '-d', 'cwd', '-p', `${pid}`], {
timeout,
});
const processCWD = stdout
.split('\n')
.find((output) => output.startsWith('n'))
?.slice(1);
return processCWD && path.isAbsolute(processCWD) ? path.normalize(processCWD) : null;
} catch {
return null;
}
}

/** Get information about a running process given a port. Returns null if no process is running on the given port. */
export function getRunningProcess(port: number): {
interface RunningProcess {
/** The PID value for the port. */
pid: number;
/** Get the directory for the running process. */
directory: string;
/** The command running the process like `node /Users/evanbacon/.../bin/expo start` or the `package.json` name like `my-app`. */
command: string;
} | null {
// 63828
const pid = getPID(port);
if (!pid) {
}

/** Get information about a running process given a port. Returns null if no process is running on the given port. */
export async function getRunningProcess(port: number): Promise<RunningProcess | null> {
// Don't even try on Windows, since `lsof` and `ps` are not available there
if (process.platform === 'win32') {
return null;
}

try {
// /Users/evanbacon/Documents/GitHub/lab/myapp
const directory = getDirectoryOfProcessById(pid);
// /Users/evanbacon/Documents/GitHub/lab/myapp/package.json
const command = getProcessCommand(pid, directory);
// TODO: Have a better message for reusing another process.
return { pid, directory, command };
} catch {
const pid = await getPID(port);
if (!pid) {
return null;
}
try {
const directory = await getDirectoryOfProcessById(pid);
if (directory) {
const command = await getProcessCommand(pid, directory);
if (command) {
return { pid, directory, command };
}
}
} catch {}
return null;
}
5 changes: 2 additions & 3 deletions packages/@expo/cli/src/utils/port.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ function isRestrictedPort(port: number) {
async function isBusyPortRunningSameProcessAsync(projectRoot: string, { port }: { port: number }) {
const { getRunningProcess } =
require('./getRunningProcess') as typeof import('./getRunningProcess');

const runningProcess = isRestrictedPort(port) ? null : getRunningProcess(port);
const runningProcess = isRestrictedPort(port) ? null : await getRunningProcess(port);
if (runningProcess) {
if (runningProcess.directory === projectRoot) {
return true;
Expand Down Expand Up @@ -91,7 +90,7 @@ export async function choosePortAsync(

const { getRunningProcess } =
require('./getRunningProcess') as typeof import('./getRunningProcess');
const runningProcess = isRestricted ? null : getRunningProcess(defaultPort);
const runningProcess = isRestricted ? null : await getRunningProcess(defaultPort);

if (runningProcess) {
const pidTag = chalk.gray(`(pid ${runningProcess.pid})`);
Expand Down
2 changes: 2 additions & 0 deletions packages/@expo/config/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

### 💡 Others

- Replace `require-from-string` and `sucrase` with `@expo/require-utils` ([#42884](https://github.com/expo/expo/pull/42884) by [@kitten](https://github.com/kitten))

## 55.0.4 — 2026-02-03

### 💡 Others
Expand Down
Loading
Loading