DEV Community

a2n
a2n

Posted on

Debugging JS/TS from Neovim: JS/TS, the NestJS special & the React Native drama queen

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
Enter fullscreen mode Exit fullscreen mode

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,
},
Enter fullscreen mode Exit fullscreen mode

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:

  1. 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
  2. DI bootstrap happens fast. Attach too late and you'll never break in AppModule (for example) because it's already initialized
  3. 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"
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

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",
},
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)