Photo by https://unsplash.com/@alexacea

Debug your Typescript module with npm link and Visual Studio Code

Boost Productivity and ditch console.log statements with npm link, source maps, and Visual Studio Code Debugger to troubleshoot your Typescript module.

Tuesday, Aug 18, 2020

avatarvscode
avatartypescript

Introduction

I've recently encountered the use case where I started to convert an npm package built previously in javascript to typescript.

Why you may ask? "Because all the cool kids are doing it!".

I've yet to set up proper unit testing, so my workflow has been creating a symlink (via npm link) from where my module is located. Making it available to be require('') or import into another project on my local machine.

During my initial debugging process I came across an issue where I wanted add breakpoints to the source code .ts files but could only get VS Code debugger to pick up the breakpoints in the transpiled .js files. Which is "okay" but I wanted to boost productivity with enabling breakpoints in my actual source files.


Setup

After quite a few frustrating hours I finally got it working on both my Windows and macOS machines with the following steps.


Attention: At the time of writing this, there is an issue with the newer version of VS Code's Js debugger (preview). In your settings, set "debug.javascript.usePreview": false

Update:

If your currently using VS Code's new Javascript Debugger (Preview), depending on when you're reading this, you may need to add a few things to .vscode / launch.json file mentioned below. Shoutout to Connor Peet on the VS Code team.


To demonstrate how to set this up we're going to create two completely separate projects:

  • Typescript module, moduleA
  • Project, projectB

First our Typescript Module that we will be debugging with breakpoints in VS Code.

mkdir moduleA

Change Directory and init package.json file

moduleA/

npm init -y

Install typescript and other dependencies

moduleA/

npm i -D typescript

Create your tsconfig.json file to set options for the typescript transpiler

moduleA / tsconfig.json

{
  "compilerOptions": {
    "outDir": "build",
    "moduleResolution": "node",
    "module": "commonjs",
    "allowJs": true,
    "target": "es5",
    "esModuleInterop": true,
    "declaration": true,
    "downlevelIteration": true
  },
  "exclude": ["node_modules"]
}

Then our typescript module, create file, "index.ts"

moduleA / index.ts

export function add(num1: number, num2: number): number {
  return num1 + num2;
}

Finally run Typescript's compiler

moduleA /

tsc

This should generate the following in your project root

moduleA / index.js

'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
exports.add = void 0;
function add(num1, num2) {
  return num1 + num2;
}
exports.add = add;

Now you can test in an existing javascript/typescript project or create a new one from scratch.

Create new directory and switch your terminal

mkdir projectB

projectB/

npm init -y

Create your project application entrypoint, if you havent done so already.

projectB / index.js

const { add } = require('moduleA');
 
add(1, 2);

Note: This currently won't work and we'll get to that in a second with npm link.



Npm and npm link (package linking)

What is npm link?

npm link is a cli command that will create a symlink in the global node_modules folder on your machine that will then link to where the command was run.

Essentially this allows us to "mock" installing the npm module as a dependency from our global node_modules directory rather from a npm published package.

In the package you wish to test locally, for this example moduleA, execute the following.

moduleA/

npm link

Now this can be installed via npm link {name} in all of your projects on your machine.

Then in your project you want to test the module in, "Project B", you would execute

projectB/

npm link moduleA

Not only is this useful for monorepo projects but for testing npm packages before publishing them. Read More

Now if you look into your node_modules directory, you'll see package directory, "moduleA" in the list with a arrow to the far right indicating a symlink was successfully created.

As you inspect the module, you'll see moduleA to its entirety. In addition, newer modifications to your typescript code will be updated to this location as well. So no need to repeat previous steps.


Setting up sourcemaps with tsconfig

Now that we have our module symlinked to our project to start using and testing, we will need to setup sourcemaps to set breakpoints in our typescript source file(s).

What is a source map?

A sourcemap is file that maps the transpiled js files back to its original source, that have either been written in other languages, newer versions of javascript, or typescript. The source map can either be a separate file that has a .map extension or be included inline as a comment in the transpiled code.

The whole purpose of this file is to allow the developer to add breakpoints and debugger statements to their original source code and enabling the browser or other debuggers (VS Code), to reconstruct the source files and present it in the debugger.

To enable this in this example we will need to modify the tsconfig.json file in moduleA.

The typescript compiler provides an option to generate sourcemaps for us, simply add "sourceMap" to true or pass as an flag to the tsc command directly, tsc --sourceMap

moduleA / tsconfig.json

{
  "compilerOptions": {
    "outDir": "build",
    "moduleResolution": "node",
    "module": "commonjs",
    "allowJs": true,
    "target": "es5",
    "esModuleInterop": true,
    "declaration": true,
    "downlevelIteration": true,
    "sourceMap": true
  },
  "exclude": ["node_modules"]
}

Next time the typescript compiler runs, additional files in the output location should have been created with a .js.map extension.

Now that sourcemaps are setup, the last part is to setup VS Code debugger via the launch.json file.


Setup launch.json for VS Code debugger

What is launch.json?

Json file usually located in the .vscode directory folder that VS Code keeps for debugging configuration information.

For our purpose we'll setup launch configuration for Node.js debugging.

projectB / .vscode / launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug typescript module",
      "program": "${workspaceFolder}/index.js"
    }
  ]
}

If all of the source files that we wish the VS Code debugger to step through was in our project (projectB) then the following would not be necessary.

Since we want to use and test our moduleA package as a symlink in our projectB project, we need to assist VS Code debugger in setting a few things in our launch.json file.


As a baseline, attempt to run the VS Code debugger.

And.... nada.

Reason it did not work that time, is because we need to add a few configurations to the launch.json file.


Now make the following changes

projectB / .vscode / launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug typescript module",
      "program": "${workspaceFolder}/index.js",
      "runtimeArgs": ["--preserve-symlinks"],
      "sourceMaps": true,
      "outFiles": ["${workspaceFolder}/node_modules/moduleA/**/*.js"]
    }
  ]
}
  • runTimeArgs:

    Arguments passed to Node.js runtime. The "--preserve-symlinks" tells Node.js to preserve any symlinks paths created via npm link.

  • sourceMaps:

    Enables the VS Code to utilize source maps if its able to find any.

  • outFiles:

    Glob patterns for locating compiled/generated javascript files from its original source. The /**/*.js is a wildcard search for any js file(s) in the moduleA package within the node_modules directory. Otherwise the debugger would not be able to pause on any breakpoints.

Update: For using VS Code's Javascript Debugger (Preview)

Prior to an upcoming fix, apply the following to your launch.json file

  • Change type from node to pwa-node
  • Add resolveSourceMapLocations with an array of [ "${workspaceFolder}/**" ]

Once the fix is live, the resolveSourceMapLocations should default to the glob pattern(s) specified in the outFiles directory.

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "pwa-node",
      "request": "launch",
      "name": "Debug typescript module",
      "program": "${workspaceFolder}/index.js",
      "runtimeArgs": ["--preserve-symlinks"],
      "sourceMaps": true,
      "outFiles": ["${workspaceFolder}/node_modules/moduleA/**/*.js"],
      "resolveSourceMapLocations": ["${workspaceFolder}/**"]
    }
  ]
}

Now try running the VS Code debugger again ;)

Success!



Conclusion

Hope this helps if you've been tripped up by this as well.

Happy Coding and Cheers!