Typescript breakpoints with VSCode
Asked Answered
M

6

6

I've this Yeoman scaffolded project from the generator-express-no-stress-typescript template. I need to debug it, as in "step through the typescript code" with Visual Studio Code (and no, console.log() is not enough for me).

As per the docs I only need to issue

npm run dev:debug

and then attach VSCode. Here is the problem: if I do that, VSCode can't bind the breakpoints. If I make the debugger stop on the first execution line ("stopOnEntry": true), it stops in the generated JavaScript code instead of the source TypeScript code, and/but the filename in the tab title shows "index.ts" (not .js) in italics.

Here is the dev:debug script target in package.json:

"dev:debug": "nodemon --exec \"node -r ts-node/register --inspect-brk\" server/index.ts | pino-pretty",

and here is the "Attach" configuration in my launch.json:

    {
        "name": "Debug (Attach)",
        "port": 9229,
        "request": "attach",
        "cwd": "${workspaceFolder}",
        "sourceMaps": true,
        "skipFiles": ["<node_internals>/**"],
        "type": "node",
        // "outFiles": ["${workspaceFolder}/dist/**/*.js"],
    },

Since this did not work, I tried a few other tutorials out there, and one (I can't remember which one) made me add the following configuration to my launch.json:

   {
       "name": "Run and debug",
       "program": "${workspaceFolder}/server/index.ts",
       "request": "launch",
       "skipFiles": [
           "<node_internals>/**"
       ],
       "type": "node",
       // "outFiles": ["${workspaceFolder}/dist/**/*.js"],
       "runtimeArgs": ["-r", "ts-node/register", "--preserve-symlinks"],
       "runtimeExecutable": "node",
       "args": ["--inspect", "${workspaceFolder}/server/index.ts"],
       "cwd": "${workspaceFolder}",           
   }

This seemed promising at first, but then, when I tried it, it stepped through JS code instead of TS, just like the "Debug (Attach)" configuration above.

Here is the generated JS code that the debugger uses to trace execution. Please note that it contains the sourcemap in the commment at its end.

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
var _a, _b;
Object.defineProperty(exports, "__esModule", { value: true });
require("./common/env");
const server_1 = __importDefault(require("./common/server"));
const routes_1 = __importDefault(require("./routes"));
const models_1 = __importDefault(require("./api/models"));
const port = parseInt((_a = process.env.PORT) !== null && _a !== void 0 ? _a : '3000');
const syncdb = ((_b = process.env.SYNC_DB_SCHEMA_ON_STARTUP) !== null && _b !== void 0 ? _b : 'false') === 'true';
if (syncdb) {
    models_1.default.sequelize.sync({ force: true }).then(() => {
        console.log("DB Aggiornato");
    }).catch((err) => {
        console.log("Errore", err);
    });
}
exports.default = new server_1.default().router(routes_1.default).listen(port);
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiL2hvbWUvbHVjaW8vbXlhcHAvc2VydmVyL2luZGV4LnRzIiwic291cmNlcyI6WyIvaG9tZS9sdWNpby9teWFwcC9zZXJ2ZXIvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7O0FBQUEsd0JBQXNCO0FBQ3RCLDZEQUFxQztBQUNyQyxzREFBOEI7QUFDOUIsMERBQThCO0FBQzlCLE1BQU0sSUFBSSxHQUFHLFFBQVEsQ0FBQyxNQUFBLE9BQU8sQ0FBQyxHQUFHLENBQUMsSUFBSSxtQ0FBSSxNQUFNLENBQUMsQ0FBQztBQUdsRCxNQUFNLE1BQU0sR0FBRyxDQUFDLE1BQUEsT0FBTyxDQUFDLEdBQUcsQ0FBQyx5QkFBeUIsbUNBQUksT0FBTyxDQUFDLEtBQUssTUFBTSxDQUFDO0FBRTdFLElBQUksTUFBTSxFQUFFO0lBQ1IsZ0JBQUUsQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLEVBQUUsS0FBSyxFQUFFLElBQUksRUFBRSxDQUFDLENBQUMsSUFBSSxDQUFDLEdBQUcsRUFBRTtRQUN6QyxPQUFPLENBQUMsR0FBRyxDQUFDLGVBQWUsQ0FBQyxDQUFDO0lBQ2pDLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLEdBQVEsRUFBRSxFQUFFO1FBQ2xCLE9BQU8sQ0FBQyxHQUFHLENBQUMsUUFBUSxFQUFFLEdBQUcsQ0FBQyxDQUFBO0lBQzlCLENBQUMsQ0FBQyxDQUFDO0NBQ047QUFHRCxrQkFBZSxJQUFJLGdCQUFNLEVBQUUsQ0FBQyxNQUFNLENBQUMsZ0JBQU0sQ0FBQyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCAnLi9jb21tb24vZW52JztcbmltcG9ydCBTZXJ2ZXIgZnJvbSAnLi9jb21tb24vc2VydmVyJztcbmltcG9ydCByb3V0ZXMgZnJvbSAnLi9yb3V0ZXMnO1xuaW1wb3J0IGRiIGZyb20gJy4vYXBpL21vZGVscyc7XG5jb25zdCBwb3J0ID0gcGFyc2VJbnQocHJvY2Vzcy5lbnYuUE9SVCA/PyAnMzAwMCcpO1xuXG5cbmNvbnN0IHN5bmNkYiA9IChwcm9jZXNzLmVudi5TWU5DX0RCX1NDSEVNQV9PTl9TVEFSVFVQID8/ICdmYWxzZScpID09PSAndHJ1ZSc7XG5cbmlmIChzeW5jZGIpIHtcbiAgICBkYi5zZXF1ZWxpemUuc3luYyh7IGZvcmNlOiB0cnVlIH0pLnRoZW4oKCkgPT4ge1xuICAgICAgICBjb25zb2xlLmxvZyhcIkRCIEFnZ2lvcm5hdG9cIik7XG4gICAgfSkuY2F0Y2goKGVycjogYW55KSA9PiB7XG4gICAgICAgIGNvbnNvbGUubG9nKFwiRXJyb3JlXCIsIGVycilcbiAgICB9KTtcbn1cblxuXG5leHBvcnQgZGVmYXVsdCBuZXcgU2VydmVyKCkucm91dGVyKHJvdXRlcykubGlzdGVuKHBvcnQpOyJdfQ==

I decoded the base64 sourcemap and it seems to contain the correct paths to my sources and even a copy of the source code:

{"version":3,"file":"/home/lucio/myapp/server/index.ts","sources":["/home/lucio/myapp/server/index.ts"],"names":[],"mappings":";;;;;;AAAA,wBAAsB;AACtB,6DAAqC;AACrC,sDAA8B;AAC9B,0DAA8B;AAC9B,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAA,OAAO,CAAC,GAAG,CAAC,IAAI,mCAAI,MAAM,CAAC,CAAC;AAGlD,MAAM,MAAM,GAAG,CAAC,MAAA,OAAO,CAAC,GAAG,CAAC,yBAAyB,mCAAI,OAAO,CAAC,KAAK,MAAM,CAAC;AAE7E,IAAI,MAAM,EAAE;IACR,gBAAE,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;QACzC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAQ,EAAE,EAAE;QAClB,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;IAC9B,CAAC,CAAC,CAAC;CACN;AAGD,kBAAe,IAAI,gBAAM,EAAE,CAAC,MAAM,CAAC,gBAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC","sourcesContent":["import './common/env';\nimport Server from './common/server';\nimport routes from './routes';\nimport db from './api/models';\nconst port = parseInt(process.env.PORT ?? '3000');\n\n\nconst syncdb = (process.env.SYNC_DB_SCHEMA_ON_STARTUP ?? 'false') === 'true';\n\nif (syncdb) {\n    db.sequelize.sync({ force: true }).then(() => {\n        console.log(\"DB Aggiornato\");\n    }).catch((err: any) => {\n        console.log(\"Errore\", err)\n  

Here is my tsconfig.json, just in case it matters:

{
  "compileOnSave": false,
  "compilerOptions": {
    "inlineSourceMap": true, // added after answer below, still doesn't work
    "target": "ES2019",
    "lib": ["ES2020"],
    "strict": true,
    "module": "commonjs",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "declaration": true,
    "moduleResolution": "node",
    "useUnknownInCatchVariables": false,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "strictNullChecks": true,
    "noUnusedParameters": true,
    "noUnusedLocals": false,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": false,
    "strictPropertyInitialization":false,
    "alwaysStrict": true,
    "outDir": "dist",
    "typeRoots": ["node_modules/@types"],
    "resolveJsonModule": true,
    "baseUrl": "."

  },
  "include": ["server/**/*.ts", "server/api/models/index.ts"],
  "exclude": ["node_modules", "./test/", "./dist"]
}

And here is the output of npx ts-node --showConfig (after beautifulcoder's comment to his answer)

{
  "ts-node": {
    "cwd": "/home/lucio/myapp",
    "projectSearchDir": "/home/lucio/myapp",
    "project": "/home/lucio/myapp/tsconfig.json"
  },
  "compilerOptions": {
    "target": "es2019",
    "lib": [
      "es2020"
    ],
    "strict": true,
    "module": "commonjs",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "useUnknownInCatchVariables": false,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "strictNullChecks": true,
    "noUnusedParameters": true,
    "noUnusedLocals": false,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": false,
    "strictPropertyInitialization": false,
    "alwaysStrict": true,
    "outDir": "./.ts-node",
    "typeRoots": [
      "/home/lucio/myapp/node_modules/@types"
    ],
    "resolveJsonModule": true,
    "baseUrl": "./",
    "inlineSourceMap": false, // PLEASE NOTE: this is false even after I added `"inlineSourceMap": true` in tsconfig.json above
    "inlineSources": true,
    "noEmit": false
  }
}

I've scaffolded a new minimal example generator-express-no-stress-typescript project, following the documented procedure again. e.g.

$ npm install -g yo generator-express-no-stress
$ yo express-no-stress ts-debug-test

and choosing "OpenAPI 3" over "Swagger 2" when asked, which is what I did with the real project when I scaffolded it the first time. If you need a minimal example showing the problem you can do that too or you can find mine at GitHub, if you prefer. I've only added my launch.json to it and you only need to set a breakpoint at the first meaningful line of code.

So now I don't know what to do next in order to debug my code... any clues?

Mobile answered 27/5, 2022 at 9:21 Comment(2)
Have you tried nuking nodemon?Congou
Not yet, I did not know it could help. What should I do exactly?Mobile
M
2

The problem was being caused (the real bug was being triggered) by a symlink in the path where I store my project, e.g.

/home/lucio/myapp -> /home/lucio/workspace/vscode/myapp

The real bug is in VSCode, here.

The straghtforward workaround is to avoid symlinks in the project folder path.

Mobile answered 9/6, 2022 at 9:31 Comment(0)
C
5

I think the outFiles property in your Debug (Attach) configuration is likely the problem. ts-node doesn't actually write its generated files and source maps to disk, so if VS Code is looking for them there then it won't find them. You should just be able to remove this property.

I have a number of codebases that use ts-node and nodemon in basically exactly the way you're using them here, and source mapping works fine. The only difference I can see is that my debug configurations don't have outFiles set.


EDIT: since the outFiles thing didn't work, the only other thing I can think of is that it's something in tsconfig.json. Here's one of mine that works. My guess is that it might be the "inlineSourceMap": true.

{
  "compilerOptions": {
    "target": "ES2018",
    "module": "commonjs",
    "strict": true,
    "jsx": "preserve",
    "importHelpers": true,
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "inlineSourceMap": true,
    "noImplicitAny": false,
    "types": [
      "webpack-env"
    ],
    "lib": [
      "esnext",
      "dom",
      "dom.iterable",
      "scripthost"
    ]
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts",
    "tests/**/*.tsx",
    "../interface/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}
Ceres answered 6/6, 2022 at 4:51 Comment(8)
Thanks, I've tried to remove outFiles from launch.json, but nothing changed. At the very least, I've understood what outFiles is supposed to do.Mobile
Ok, I've added my tsconfig.json to the answer in case that helps.Ceres
Edited my question. Please note the comment in the npx ts-node --showConfig output. I don't know why it is still showing false.Mobile
I even tried your tsconfig.json but it did not solve the problem in my case.Mobile
I hate wasting reputation points and they're about to expire, so I award them to you, without accepting your answer, because, despite your answer did not solve my problem, your suggestions helped me a lot in understanding how things work. The bounty aside, if you have any other suggestions they are very welcome, thanks.Mobile
Thanks! Sorry I couldn't be of more assistance.Ceres
Adding "inlineSourceMap": true solved it for me. I was dealing with the same problem, trying to debug strapi 4.8.1 + typescript inside docker containerCornhusk
Yeah this solution works for me (basic typescript project in vscode). "sourceMap": true doesn't seem to work. Idk if it made a difference but I also had to manually clean my build to get rid of the old source map files.Dorinedorion
M
2

The problem was being caused (the real bug was being triggered) by a symlink in the path where I store my project, e.g.

/home/lucio/myapp -> /home/lucio/workspace/vscode/myapp

The real bug is in VSCode, here.

The straghtforward workaround is to avoid symlinks in the project folder path.

Mobile answered 9/6, 2022 at 9:31 Comment(0)
C
1

My current theory is you have the debug script running behind nodemon. This is a node process monitor meant for production and not for debugging purposes on local.

Try:

{"dev:debug": "node -r ts-node/register --inspect-brk server/index.ts"}
Congou answered 3/6, 2022 at 19:59 Comment(6)
Just tried, thanks but no dice: it traces the generated JS code instead of the source TS, e.g. just the same problem as above.Mobile
I bet ts-node doesn't spit out source maps. There is probably a way to do this.Congou
npmjs.com/package/ts-node#configuration-1Congou
I've edited my question adding the output of npx ts-node --showConfig. I don't know what I should look for in that output, except "sourceMap": true which is already there.Mobile
Yes, this is correct and it looks good. I'm stumped, can you post minimal reproducible codes on GitHub? I'm actually morbidly curious.Congou
I've published a minimal example here: github.com/lucrus73/ts-debug-test and updated my question accordingly.Mobile
C
1

As I just figured out - this might be due to a VSCode console not utilizing proper node version.

In case you are using nvm (or something similar) - this can easily be the case.

Setup wasn't working anyhow, with any options enumerated here or in any other similar threads (meaning it did started, but breakpoints where not hit).

But as soon as I added proper version to my launch.json (in my case "runtimeVersion": "12.22.12") - everything started to work with simplest defaults...

For the referrence:

My "default" node version was 8.x.x.

launch.json

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "runtimeVersion": "12.22.12",
            "request": "launch",
            "name": "Launch Program",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "program": "${workspaceFolder}/src/server.ts",
            "preLaunchTask": "tsc: build - tsconfig.json",
            "sourceMaps": true,
            "smartStep": true,
            "internalConsoleOptions": "openOnSessionStart",
            "runtimeExecutable": "node",
            "outFiles": [
                "${workspaceFolder}/dist/**/*.js"
            ]
        }
    ]
}

tsconfig.json

{
    "extends": "@tsconfig/node12/tsconfig.json",
    "compilerOptions": {
        "outDir": "dist",
        "sourceMap": true
    },
    "include": ["src"],
    "exclude": ["node_modules"]
}
Creaturely answered 2/10, 2022 at 13:16 Comment(1)
Thanks in advance, adding "runtimeVersion" in launch.json was the only thing that solved my problem. I'm using VSCode on a Mac, and i use nvm to manage multiple node versions and i just could not make the debugger to run and stop any breakpoint.Indictment
B
0

I checked all the solutions and none of them worked for my case. at last, I cleared the "outFiles" line in the launch.json and it worked!

just comment out these lines in the launch.json:

  "outFiles": [
      "${workspaceFolder}/dist/**/*.js"
  ]
Barrack answered 6/7 at 7:51 Comment(0)
C
0

Typically I debug npm scripts using the "Javascript Debug Terminal" which will auto-attach and bind breakpoints when using npm run [script] commands.

If you're using a package like tsx or ts-node to strip types then you need to make sure source maps are enabled in your tsconfig.json by setting inlineSourceMap to true.

I've also run into issues getting the breakpoints to bind correctly when I load node-options from my .npmrc file. The breakpoints bind correctly without .npmrc, but as a workaround using node --run [script] instead of npm run [script] with node v22+ worked as well.

Edit

Apparently vs code requires its own NODE_OPTIONS variables for debugging: https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_how-can-i-set-nodeoptions

So you need to append to it in your shell configuration file:

export NODE_OPTIONS="$NODE_OPTIONS --no-warnings"

or in your project's .npmrc file:

node-options="${NODE_OPTIONS} --no-warnings"

Edit 2

You can now run node (v22.7+) with --experimental-strip-types (simple type stripping) or --experimental-transform-types (transpiles enums etc.) to run typescript. So if you add the following to .npmrc:

node-options="${NODE_OPTIONS} --experimental-strip-types"

Then you can just run node normally in package.json:

{
  ...
  "scripts": {
    ...
    "dev:debug": "node server/index.ts",
   }
}

Finally open up a Javascript Debug Terminal in VS Code and just run npm run dev:debug

Cost answered 9/10 at 18:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.