Using source-map-loader with TypeScript

Published: Monday, November 16, 2020

Greetings, friends! TypeScript is all the rage these days. It makes our code cleaner, catches errors at compile-time instead of runtime, and helps prevent those nasty "Uncaught TypeError: blahblah is not a function" errors. When working with third party TypeScript libraries, I like getting IntelliSense inside VS Code, but sometimes, it'd be nice to view the original TypeScript source code while I'm debugging in Chrome dev tools. That's where the Source Map Loader comes in handy!

The source-map-loader is a very useful tool for extracting source maps from third party libraries you have installed such as those inside your node_modules directory. If the third party libraries don't contain source maps, then you'll have to find a different approach. Maybe the library contains both bundled .js files as well as the original .ts files? In that case, you can use Babel's TypeScript preset in conjunction with Webpack to create source maps for you. However, you won't always see the original TypeScript files in the node_modules directory. You'll likely find only JavaScript files and their corresponding declaration files, generated by the TypeScript compiler, tsc. You may have to look at a library author's GitHub page to see the original TypeScript code. For this reason, if you're building your own TypeScript library, it's important that you also include source maps.

For this tutorial, I have created a repo on GitHub. If you would like to follow along, please clone or download this repo:

shell
Copied! ⭐️
git clone https://github.com/inspirnathan/source-map-loader-with-typescript-tutorial.git

There are two projects: "my-app" and "my-typescript-library." You only have to run npm install inside the my-app directory. The package.json file inside the my-typescript-library is only there to provide information to npm and the TypeScript compiler.

You should see the following folder structure:

project structure of the cloned git repository

Let's examine the my-typescript-library folder first. Inside, there is a src directory that contains two files.

Inside my-typescript-library/src/greetings.ts, I'm exporting four ways to say "hello" in different languages.

typescript
Copied! ⭐️
export function sayHelloEnglish(): string {
  return 'Hello!';
}

export function sayHelloSpanish(): string {
  return 'Hola!';
}

export function sayHelloFrench(): string {
  return 'Bonjour!';
}

export function sayHelloPersian(): string {
  return 'Salaam!';
}

Inside my-typescript-library/src/index.ts, we are simply exporting each of these modules:

typescript
Copied! ⭐️
export {
  sayHelloEnglish,
  sayHelloSpanish,
  sayHelloFrench,
  sayHelloPersian,
} from './greetings';

We can transpile these TypeScript files down to JavaScript using the following command inside my-typescript-library:

shell
Copied! ⭐️
npm run build

The above command will run the TypeScript compiler using the configuration found in tsconfig.json:

json
Copied! ⭐️
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es2015",
    "declaration": true,
    "outDir": "./dist",
    "sourceMap": true
  },
  "include": [
    "src/**/*"
  ]
}

When the build command is finished, we should see the following files inside the newly generated dist directory.

md
Copied! ⭐️
greetings.d.ts
greetings.js
greetings.js.map
index.d.ts
index.js
index.js.map

It's important to notice that we have generated source maps using the TypeScript compiler. These source maps will eventually be picked up by the source-map-loader we will use later. If you are using a third party library built in TypeScript, and they do not have a source map, then you won't be able to see source maps (that link back to the original TypeScript code) appear in your browser.

Now, let's look at the my-app folder and inspect the my-app/package.json file:

json
Copied! ⭐️
{
  "name": "my-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "serve .",
    "build": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "my-typescript-library": "file:../my-typescript-library"
  },
  "devDependencies": {
    "serve": "^11.3.2",
    "source-map-loader": "^1.1.2",
    "webpack": "^5.4.0",
    "webpack-cli": "^4.2.0"
  }
}

Notice that we're installing my-typescript-library from the folder itself. This is a neat little trick to install packages you've developed locally without having to use npm link. However, I would suggest you use "npm link" when working on a professional project or developing a command-line tool as it can prove more useful, but it's okay to avoid it in this tutorial.

Alright. Let's build and start the application. While inside the my-app directory, run the following command:

shell
Copied! ⭐️
npm run build && npm start

This will build the application using Webpack followed by starting a small server that serves up the index.html page.

greetings in different languages

Let's look at the my-app/index.html source code:

html
Copied! ⭐️
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My App</title>
</head>
<body>
  <h1>Greetings in Different Languages</h1>
  <div id="root"></div>
  <script src="./dist/main.js"></script>
</body>
</html>

We are injecting code inside the root element using a script from my-app/dist/main.js. This file is the compiled Webpack bundle, so let's look at the webpack.config.js file to figure out where the entry point is.

javascript
Copied! ⭐️
module.exports = {
  mode: 'development',
  devtool: 'source-map',
  entry: './src/index.js',
  module: {
    rules: [
      {
        test: /\.js$/,
        enforce: 'pre',
        use: ['source-map-loader'],
      },
    ],
  },
}

Inside this Webpack config, you can see that the entry file is my-app/src/index.js. That is the file that is injecting HTML inside the root element we saw in index.html. Notice that I have configured the source-map-loader for you. This is a Webpack loader that can grab all the source maps inside your project as well as those inside the node_modules directory. The pre option in the enforce field means that this loader should execute before all other loaders (though the order is a bit more complex than that as seen in Webpack's documentation).

With the power of source maps combined, let's see the source-map-loader in action! Open up Chrome Developer Tools and go to the "Sources" tab. Then, you can see the original TypeScript files found in my-typescript-library.

debugging the original TypeScript source code in Chrome Dev Tools

Pretty cool! We can inspect the original TypeScript source code of files found inside our node_modules directory. We can also insert breakpoints inside the my-app/src/index.js file, which is the file responsible for injecting HTML into the page:

javascript
Copied! ⭐️
import * as greetings from 'my-typescript-library';

const root = document.getElementById('root');

root.innerHTML = `
  <h2>${greetings.sayHelloEnglish()}</h2>
  <h2>${greetings.sayHelloSpanish()}</h2>
  <h2>${greetings.sayHelloFrench()}</h2>
  <h2>${greetings.sayHelloPersian()}</h2>`;

The source-map-loader works on all types of files that contain a sourceMappingURL, not just TypeScript and JavaScript files. As long as a package provides a source map, then Webpack can scoop it up with the source-map-loader.

Conclusion

Source maps are very important for debugging purposes. With all the transformations that happen in the JavaScript world, it's crucial to have the ability to see original source code of a library. Although it's entirely possible to navigate to the library author's GitHub page to inspect the source code, it's more convenient to troubleshoot code on the fly in the browser's developer tools. It's faster and easier to understand the issues that may exist as we're developing our application. If you have source maps visible in production applications, then you can see if third party code is causing any bugs or issues. This is the power of source maps. 😎

Resources