-
Notifications
You must be signed in to change notification settings - Fork 0
Example Expo setup
Expo or React Native development is likely to be one of the main use cases for simctl.nvim, although the following examples could also apply to web development targeting the iOS Simulator.
Using NeoVim for developing Expo apps works great, but as with VS Code, I often find myself Command-Tabbing over to the iOS Simulator and using the mouse to clumsily force quit the running Expo Go app. Sometimes I also need to start from scratch and have to go through the somewhat cumbersome delete and reinstall app process. Too many mouse clicks for my liking.
In my personal setup I combine vimux with simctl.nvim to provide a dev setup for Expo that I can trigger with shortcuts. If you don't use tmux you could use a tool like toggleterm.nvim for instance, but I have found the tmux integration to be the most stable.
I've set up Vimux to reuse the same tmux pane by giving it a fixed VimuxRunnerName, and I want my code and my runner to be side by side:
vim.g.VimuxRunnerName = "vimuxout"
vim.g.VimuxOrientation = "h"
I've made a simple module in ~/.config/nvim/lua/user/expo.lua with some Expo-specific run tasks. You can place this anywhere and adjust your import accordingly. This uses yarn, so adjust the commands if you use npm or other tools.
local M = {}
--- Quit Expo Go on running iOS Simulators
-- @param callback function to call when command finishes
M.quit = function(callback)
require("simctl.api").terminate({ deviceId = "booted", appId = "host.exp.Exponent" }, callback)
end
--- Uninstall Expo Go from running iOS Simulators
-- @param callback function to call when command finishes
M.uninstall = function(callback)
require("simctl.api").uninstall({ deviceId = "booted", appId = "host.exp.Exponent" }, callback)
end
--- Send the reload command r to Expo Go running in a Vimux window
M.reload = function()
vim.cmd("VimuxRunCommand 'r'")
end
--- Helper function to check if we have a booted device up and running
local hasBootedDevice = function(deviceList)
for _, device in ipairs(deviceList) do
if device.state == "Booted" then
return true
end
end
return false
end
M.run = function()
-- Stop the Expo Go process if it is running
if vim.g.VimuxRunnerIndex then
vim.cmd "VimuxInterruptRunner"
end
local simctl = require "simctl.api"
local aw = require("simctl.lib.async")
aw.async(function()
-- Check if we have a booted device to run on, boot one if not
local _, devices = aw.await(simctl.list)
if not hasBootedDevice(devices) then
local success = aw.await(function(cb)
simctl.boot({}, cb)
end)
if not success then
-- No device was selected, cancel
return
end
end
-- Run the app
vim.schedule(function()
vim.cmd("VimuxRunCommand 'yarn start --ios'")
end)
end)
end
return MI can now assign these actions to shortcuts using which-key.nvim:
local wk = require("which-key")
wk.register {
["<leader>me"] = { require("user.expo").run, "Expo Run" },
["<leader>mq"] = { require("user.expo").quit, "Quit Expo Go" },
["<leader>mr"] = { require("user.expo").reload, "Expo Reload" },
["<leader>mu"] = { require("user.expo").uninstall, "Expo Go Uninstall" },
}Now when I press <leader>me, simctl.nvim prompts me to boot up a Simulator if one is not already running. After that, Expo starts in a tmux pane and runs my app on the booted Simulator. <leader>mr sends a reload r to the Expo runner without me having to leave NeoVim but I can also interact with the runner in the usual way in the separate tmux pane. If I need a hard refresh I can run <leader>mq to quit Expo Go before running again, or <leader>mu to uninstall Expo Go completely from the device.
The above example uses the async/await pattern that simctl.nvim uses internally. You can accomplish the same with only callbacks if you find that easier to read.
To make this work in a monorepo containing several Expo app we need to expand the runner slightly. First, add three helper functions:
local map = function(tbl, func)
local newtbl = {}
for i, v in ipairs(tbl) do
newtbl[i] = func(v)
end
return newtbl
end
--- Select app name in a monorepo from a list
-- Looks for all apps under ./apps directory
-- @param callback function to call with selected app slug
local selectApp = function(callback)
local scan = require "plenary.scandir"
local apps = scan.scan_dir("./apps", { hidden = false, depth = 1, only_dirs = true })
local slugs = map(apps, function(v)
return v:match "([^/]-)/?$"
end)
vim.ui.select(slugs, { prompt = "Select app" }, function(selected)
callback(selected)
end)
end
local selectAppAndRun = function()
local simctl = require("simctl.api")
local aw = require("simctl.lib.async")
aw.async(function()
-- Quit expo go if it is running
local success, apps = aw.await(function(cb)
simctl.listapps({ deviceId = "booted" }, cb)
end)
if not success then
vim.notify("Error listing apps", vim.log.levels.ERROR)
return
end
if findAppByBundleIdentifier(apps, "host.exp.Exponent") then
aw.await(function(cb)
simctl.terminate({ deviceId = "booted", appId = "host.exp.Exponent" }, cb)
end)
end
-- Select app from monorepo and run it using Vimux
vim.schedule(function()
selectApp(function(slug)
vim.cmd("VimuxRunCommand 'yarn " .. slug .. ":start --ios'")
end)
end)
end)
endAnd then update the M.run function to use the new selectAppAndRun helper:
M.run = function()
if vim.g.VimuxRunnerIndex then
vim.cmd "VimuxInterruptRunner"
end
local simctl = require("simctl.api")
local aw = require("simctl.lib.async")
aw.async(function()
-- Check if we have a booted device to run on, boot one if not
local _, devices = aw.await(simctl.list)
if not hasBootedDevice(devices) then
local success = aw.await(function(cb)
simctl.boot({}, cb)
end)
if not success then
-- No device was selected, cancel
return
end
end
selectAppAndRun()
end)
endThe same which-key shortcuts as above will still work the same, except now you get a selector for which app you want to run before Expo kicks off.