So today let's talk about getting real breakpoints in Neovim. Not console.log, actual pause-here-inspect-the-variables debugging.
The good news is: for plain Node/TS it's almost boringly simple.
Then NestJS throws a small curveball.
Aaaaand [...] - hum - react native decides it's the main character (that sad since I started that to actually debug in react native).
Step 1: install js-debug
We first need to install the adapter, this is a server we run with node.
You can download the tar.gz build in github and unpack it in a choosen folder (I choose to create a .dap folder into my home):
mkdir ~/.dap
tar -xvzf js-debug-dap-v1.117.0.tar.gz -C ~/.dap
Were going to point nvim's adapter at that dapDebugServer.js just after now :)
Step 2: the actual nvim config
Just a side note, for React Native we need to add the nvim-dap-react-native plugin, it builds a "direct to Hermes" adapter on top of our pwa-node adapter.
Here's the whole config:
{
"AkisArou/nvim-dap-react-native",
build = "npm ci",
},
-- check https://codeberg.org/mfussenegger/nvim-dap/wiki/Debug-Adapter-installation#vscode-js-debug
{
"mfussenegger/nvim-dap",
lazy = false,
config = function()
local dap = require("dap")
dap.adapters["pwa-node"] = {
type = "server",
host = "localhost",
port = "${port}",
executable = {
command = "node",
args = { "~/.dap/js-debug/src/dapDebugServer.js", "${port}" },
},
}
dap.adapters["reactnativedirect"] = require("dap-react-native").create_adapter(dap.adapters["pwa-node"])
for _, ft in ipairs({ "javascript", "typescript", "javascriptreact", "typescriptreact" }) do
dap.configurations[ft] = dap.configurations[ft] or {}
table.insert(dap.configurations[ft], {
type = "pwa-node",
request = "launch",
name = "Launch file",
program = "${file}",
cwd = "${workspaceFolder}",
})
table.insert(dap.configurations[ft], {
type = "pwa-node",
request = "launch",
name = "Launch file (tsx)",
runtimeExecutable = "tsx",
program = "${file}",
cwd = "${workspaceFolder}",
})
table.insert(dap.configurations[ft], {
type = "pwa-node",
request = "attach",
name = "Attach NestJS (9229)",
cwd = "${workspaceFolder}",
port = 9229,
})
table.insert(dap.configurations[ft], {
type = "reactnativedirect",
request = "attach",
name = "RN: Attach Hermes",
cwd = "${workspaceFolder}",
})
end
local keymap = vim.keymap.set
keymap("n", "<leader>db", dap.toggle_breakpoint, { desc = "Toggle breakpoint" })
keymap("n", "<leader>dc", dap.continue, { desc = "Continue / launch" })
keymap("n", "<leader>do", dap.step_over, { desc = "Step over" })
keymap("n", "<leader>di", dap.step_into, { desc = "Step into" })
keymap("n", "<leader>du", dap.step_out, { desc = "Step out" })
keymap("n", "<leader>dt", dap.terminate, { desc = "Terminate session" })
keymap("n", "<leader>dr", function() dap.repl.toggle() end, { desc = "DAP repl" })
end,
},
What's happening: one pwa-node adapter for everything JS, and a react native direct adapter created from it for Hermes.
Four configurations: launch plain, launch with tsx, attach Nest, attach RN.
Step 3: Node and TypeScript
Open a .ts file, hit <leader>db to toggle a breakpoint, <leader>dc to launch.
If you're running a TS file directly, use "Launch file (tsx)", it runs node tsx under the hood and needs no manual build step.
The one trick: debugging a compiled process only shows you JS unless you tell js-debug about sourcemaps. That's why the launch configs work with .ts files directly. Js-debug resolves them via sourcemaps.
Step 4: NestJS, the special part
NestJS is still Node, so the same pwa-node stack works. But there are three gotchas:
- You debug a compiled app. Nest builds to
dist/and runs that. Attach like a plain script and you see compiled JS, not your decorators - DI bootstrap happens fast. Attach too late and you'll never break in AppModule (for example) because it's already initialized
- So you attach before it boots
Fortunately Nest CLI has a debug flag for that, that is already there in the npm run scripts:
"start:debug": "nest start --debug --watch"
Start Nest with the previous command, then <leader>dc and pick "Attach NestJS (9229)".
The flow is something like that:
nvim - DAP ▶ js-debug ─ CDP ▶ nest (runs dist/, sourcemaps map back to .ts)
Step 5: Ok now the diva: React Native and why it doesn't work (yet)
Same config as Node, just with the RN adapter added before:
{
"AkisArou/nvim-dap-react-native",
build = "npm ci",
},
It builds a reactnativedirect adapter on top of our pwa-node one.
So: start the app, <leader>db to add a breakpoint, <leader>dc and pick "RN: Attach Hermes".
And then... "Debugger and device timed out". Ha (:
Since React Native 0.76, the debugger is React Native DevTools, a Chromium window. And that window is the CDP client, not a proxy. Hermes only opens its debug socket for that window, and the window consumes it.
So our adapter shows up, finds no target and gives up:
nvim ─ DAP ▶ reactnativedirect ─ CDP ▶ Hermes
▲
Hermes only talks to the DevTools window ─┘
our adapter: "Debugger and device timed out"
Before 0.76 there was a seam (Metro's debugger proxy) external adapters could ride on. The 0.76 rewrite removed it.
So: it's not nvim's fault, nor js-debug's fault ; the last hop of the chain only speaks to one client, and that client is a window.
The fix is re-exposing a stable CDP endpoint for external debuggers and that actually sitting in an open PRs on microsoft/vscode-react-native repo (#2781/#2782).
Until it lands, RN: Attach Hermes is a coin flip, even more so on RN 0.85 (Expo SDK 56).
But, hey, at least your config is already setup!
So I let you there, have fun!
Top comments (0)