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
8 changes: 7 additions & 1 deletion packages/@expo/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@

### 🎉 New features

- Prefix web client logs with platform name in Metro terminal output. ([#45516](https://github.com/expo/expo/pull/45516) by [@EvanBacon](https://github.com/EvanBacon))

### 🐛 Bug fixes

- Fix Metro progress bars appearing as permanent output due to cursor corruption from stderr writes and stale status snapshots. ([#45523](https://github.com/expo/expo/pull/45523) by [@EvanBacon](https://github.com/EvanBacon))
- Prevent Metro loading indicator from showing broken states in headless runs. ([#45513](https://github.com/expo/expo/pull/45513) by [@EvanBacon](https://github.com/EvanBacon))
- Fix `--port 0` exiting silently in `expo start` when the port is busy. ([#45513](https://github.com/expo/expo/pull/45513) by [@EvanBacon](https://github.com/EvanBacon))

- Apply printf-style format substitution for web client logs forwarded from the browser. ([#45516](https://github.com/expo/expo/pull/45516) by [@EvanBacon](https://github.com/EvanBacon))
### 💡 Others

- Replace deprecated `url.parse()` with WHATWG `URL` API in Metro dev server. ([#45524](https://github.com/expo/expo/pull/45524) by [@EvanBacon](https://github.com/EvanBacon))
- Remove pinned dependencies ([#45520](https://github.com/expo/expo/pull/45520) by [@kitten](https://githun.com/kitten))

## 56.0.6 — 2026-05-07

### 🐛 Bug fixes
Expand Down
6 changes: 3 additions & 3 deletions packages/@expo/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@
"@expo/devcert": "^1.2.1",
"@expo/env": "workspace:~2.2.0",
"@expo/image-utils": "workspace:^0.9.0",
"@expo/inline-modules": "workspace:0.0.4",
"@expo/inline-modules": "workspace:^0.0.4",
"@expo/json-file": "workspace:^10.1.0",
"@expo/log-box": "workspace:56.0.5",
"@expo/log-box": "workspace:^56.0.5",
"@expo/metro": "~56.0.0",
"@expo/metro-config": "workspace:~56.0.4",
"@expo/metro-file-map": "workspace:56.0.0-2",
"@expo/metro-file-map": "workspace:^56.0.0-2",
"@expo/osascript": "workspace:^2.5.0",
"@expo/package-manager": "workspace:^1.11.0",
"@expo/plist": "workspace:^0.6.0",
Expand Down
61 changes: 55 additions & 6 deletions packages/@expo/cli/src/start/server/metro/MetroTerminalReporter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Terminal } from '@expo/metro/metro-core';
import chalk from 'chalk';
import path from 'path';
import { stripVTControlCharacters } from 'util';
import { format as utilFormat, stripVTControlCharacters } from 'util';

import { logWarning, TerminalReporter } from './TerminalReporter';
import type {
Expand Down Expand Up @@ -335,8 +335,17 @@ export class MetroTerminalReporter extends TerminalReporter {
}
}

#onClientLog(evt: { type: 'client_log'; level?: ClientLogLevel; data: unknown[] }) {
const { level = 'log', data } = evt;
#onClientLog(evt: {
type: 'client_log';
level?: ClientLogLevel;
data: unknown[];
mode?: string;
}) {
const { level = 'log' } = evt;
// Apply printf-style format substitution (e.g. %s, %d) that browsers handle
// natively in console methods but Node/Metro terminal logging does not.
const data = applyConsoleFormatting(evt.data);
const platformTag = getPlatformTagForClientLog(evt.mode);
if (level === 'warn' || (level as string) === 'error') {
let hasStack = false;
const parsed = data.map((msg) => {
Expand Down Expand Up @@ -399,15 +408,15 @@ export class MetroTerminalReporter extends TerminalReporter {
: symbolicated;

event('client_log', { level, data: symbolicated });
logLikeMetro(this.terminal.log.bind(this.terminal), level, null, ...filtered);
logLikeMetro(this.terminal.log.bind(this.terminal), level, platformTag, ...filtered);
})();
return;
}
}

event('client_log', { level, data });
// Overwrite the Metro terminal logging so we can improve the warnings, symbolicate stacks, and inject extra info.
logLikeMetro(this.terminal.log.bind(this.terminal), level, null, ...data);
logLikeMetro(this.terminal.log.bind(this.terminal), level, platformTag, ...data);
}

#captureLog(evt: TerminalReportableEvent) {
Expand Down Expand Up @@ -552,11 +561,51 @@ function isAppRegistryStartupMessage(body: any[]): boolean {
);
}

/** Apply printf-style format substitutions (%s, %d, %i, %f, %o, %O) that browsers handle natively */
function applyConsoleFormatting(data: unknown[]): unknown[] {
if (data.length <= 1 || typeof data[0] !== 'string' || !/%[sdifoO%]/.test(data[0])) {
return data;
}
return [utilFormat(...(data as [string, ...unknown[]]))];
}

/** @returns formatted platform name for a client log event, or null if no prefix should be shown */
function getPlatformTagForClientLog(mode?: string): string | null {
switch (mode) {
case 'ios':
return 'iOS';
case 'android':
return 'Android';
case 'web':
return 'Web';
case 'dom':
return 'DOM';
default:
return null;
}
}

/** @returns platform specific tag for a `BundleDetails` object */
function getPlatformTagForBuildDetails(bundleDetails?: BundleDetails | null): string {
const platform = bundleDetails?.platform ?? null;
if (platform) {
const formatted = { ios: 'iOS', android: 'Android', web: 'Web' }[platform] || platform;
let formatted: string;
switch (platform) {
case 'ios':
formatted = 'iOS';
break;
case 'android':
formatted = 'Android';
break;
case 'web':
formatted = 'Web';
break;
case 'dom':
formatted = 'DOM';
break;
default:
formatted = platform;
}
return `${chalk.bold(formatted)} `;
}

Expand Down
23 changes: 23 additions & 0 deletions packages/@expo/cli/src/start/server/metro/TerminalReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,29 @@ export class TerminalReporter extends XTerminalReporter implements TerminalRepor
/** Keep track of bundle processes that should not be logged. */
_hiddenBundleEvents: Set<string> = new Set();

/**
* Override Metro's update() to clear the status before _log() runs.
*
* Metro's Terminal.#update() is async and snapshots #nextStatusStr at the start.
* When _log() calls terminal.log(), Terminal starts #update() which captures
* whatever status is currently set — often a stale progress bar. This progress bar
* gets written as permanent output between log lines because the next #update()
* cycle (which would clear it) runs 33ms later.
*
* By clearing the status to empty before _log(), Terminal's #update() captures
* an empty status and doesn't write any progress bars alongside log lines.
* The correct status is then restored by terminal.status() at the end.
*/
update(event: TerminalReportableEvent): void {
if (
event.type !== 'bundle_transform_progressed' &&
event.type !== ('bundle_transform_progressed_throttled' as string)
) {
this.terminal.status('');
}
super.update(event);
}

_log(event: TerminalReportableEvent): void {
switch (event.type) {
case 'transform_cache_reset':
Expand Down
Loading
Loading