Real-time Expo & React Native Metro runtime status tracking for Neovim (Overseer + Lualine).
When running Expo / React Native in background task runners like Overseer:
Metro process status: RUNNING
Expo app state: ERROR (SyntaxError / ModuleResolution / InvariantViolation)
Overseer only tracks the process exit code. Because Metro stays alive and waits for fixes, standard task status remains RUNNING, leaving you completely blind to syntax errors, import failures, and build crashes until you switch windows or open logs.
expo-status.nvim bridges this gap:
- It intercepts Metro stdout/stderr streams in real time.
- It tracks bundling lifecycles across platforms (Android, iOS, Web, and Expo Router
λ Bundledserver routes). - It immediately reflects errors in your Neovim statusline with diagnostic highlights.
- When you fix the bug and save (
:w), it automatically clears the error state and triggers Metro reload (r)!
NORMAL │ main │ app/index.tsx │ EXPO │ typescript │ 34:12
| State | Indicator | Description |
|---|---|---|
| Starting | EXPO |
Metro server is booting up or rebundling |
| Running | ● EXPO |
Metro bundler is active and waiting for connections |
| Ok | EXPO |
Bundle completed successfully (Android, iOS, Web, λ routes) |
| Error | EXPO |
Syntax error, module resolution failure, or runtime crash |
| Stopped | (hidden) | Task stopped or disposed |
- ⚡ Live Stream Parsing: Handles ANSI escape codes and
\rcarriage return terminal overwrite animations in chronological order. - 🔄 Auto Rebuild on Save: Saving a file in the project directory while in error state automatically signals Metro to rebundle (
r) and clears the sticky error. - 🏗️ Monorepo & Workspace Aware: Automatically detects Expo projects inside
pnpm-workspace.yaml,package.jsonworkspaces, or standard monorepo layouts (apps/expo,apps/mobile, etc.). - 🟢 NVM / Node Version Management: Runs tasks with a specific Node version (e.g. Node 22 via
nvm) without needing shell hooks or manual environment switches. - 🎨 Native Lualine Component: Drop-in statusline helper with dynamic colors linked to
DiagnosticOk,DiagnosticError,DiagnosticWarn, andDiagnosticInfo. - ⌨️ Interactive Control:
:ExpoReloadcommand to trigger instant Metro reload (r) anytime without focusing the terminal window. - 🪟 Cross-Platform: First-class support for Windows, macOS, and Linux without external shell dependencies on Windows.
- Neovim
>= 0.10 - stevearc/overseer.nvim
- nvim-lualine/lualine.nvim (optional, but recommended)
return {
{
"dytra/expo-status.nvim",
dependencies = {
"stevearc/overseer.nvim",
"nvim-lualine/lualine.nvim",
},
opts = {
node_version = false, -- false to use system node (or "22" with nvm on Unix)
auto_reload_on_save = true,
},
keys = {
{ "<leader>or", "<cmd>ExpoReload<cr>", desc = "Expo: reload Metro" },
},
},
-- Ensure Overseer includes the expo template if templates are customized
{
"stevearc/overseer.nvim",
opts = {
templates = { "builtin", "expo" },
},
},
-- Add to Lualine
{
"nvim-lualine/lualine.nvim",
opts = function(_, opts)
opts.sections = opts.sections or {}
opts.sections.lualine_x = opts.sections.lualine_x or {}
table.insert(opts.sections.lualine_x, 1, require("expo-status").lualine())
end,
},
}Pass options to setup() (or via opts = { ... } in lazy.nvim):
require("expo-status").setup({
-- Node.js version to select via NVM (e.g. "22", "lts/*", or false to skip nvm)
node_version = "22",
-- Path to NVM directory (defaults to $NVM_DIR or $HOME/.nvm)
nvm_dir = nil,
-- Package manager to use: "auto" | "npm" | "pnpm" | "yarn" | "bun"
-- "auto" inspects lockfiles (pnpm-lock.yaml, yarn.lock, bun.lockb, package-lock.json)
package_manager = "auto",
-- Script name in package.json to run (e.g. "start" -> npm run start)
start_script = "start",
-- Automatically send 'r' (reload) to Metro when saving a file during error state
auto_reload_on_save = true,
-- Register the user command :ExpoReload
enable_user_command = true,
-- Status icons
icons = {
starting = " ",
running = "● ",
ok = " ",
error = " ",
},
-- Diagnostic highlight groups for statusline
colors = {
starting = "DiagnosticWarn",
running = "DiagnosticInfo",
ok = "DiagnosticOk",
error = "DiagnosticError",
},
})- Open your Expo project in Neovim:
cd my-expo-project nvim .
- Open the Overseer task runner:
<leader>oo (or :OverseerRun) - Select
Expo: start(orExpo: start (apps/expo)in monorepos). - Watch the statusline transition from
EXPOto● EXPOor EXPO.
- Introduce an intentional syntax or module error in your React Native code.
- The indicator turns red:
EXPO. - Fix the code and save (
:w). - The plugin immediately changes the state to
EXPOand sendsrto Metro. - As soon as Metro re-bundles successfully, it turns green:
EXPO.
Trigger a manual Metro reload anytime with:
:ExpoReloadOr bind it to a keymap:
vim.keymap.set("n", "<leader>or", "<cmd>ExpoReload<cr>", { desc = "Expo: reload Metro" })If you use a custom statusline (Heirline, Mini.statusline, or native):
expo-status.nvim exposes the current state globally via:
vim.g.expo_runtime_status -- "starting" | "running" | "ok" | "error" | nilAnd via Lua API:
local expo = require("expo-status")
local current_status = expo.get_status()