Hi, I am Sanjeet Tiwari...
Let's talk about

Back to Blog

Source Maps In Production

If you are not aware what source maps are, let me first give you a bit of a context and an introduction to source maps.

Introduction

Any website at its core is nothing but a combination of -

In many of our company projects, we don’t just start writing browser-readable JavaScript code in a file, and then add all these files separately one by one in the HTML doc, right ?

We use a bundler, most commonly Webpack which bundles all our JavaScript together in a single distributable JS file which is added onto the HTML via <script> tag.

Now, the speed and performance of the website directly depends on the size of these bundled JS files which may not just contain your code, but also the bulky code from any dependencies you might have installed and utilized.

Webpack itself solves this problem by automatically minifying the code via TerserPlugin in production mode. Keep in mind, minification only happens by default on Webpack version 4 and onwards.

This reduces the size of all your JavaScript by a whole lot, which in turn increases the performance of your website.

So, then what’s the problem ?? Well, your code can then, look something like this -

function(n,t,r,e,u,i,o){switch(r){case L:if(n.byteLength!=t.byteLength||n.byteOffset!=t.byteOffset)
return!1;n=n.buffer,t=t.buffer;case S:return!(n.byteLength!=t.byteLength||!i(new Pn(n),new Pn(t)));
case y:case d:case j:return Do(+n,+t);case b:return n.name==t.name&&n.message==t.message;
case O:case R:return n==t+"";case x:var f=ir;case I:var a=1&e;if(f||(f=ar),n.size!=t.size&&!a)
return!1;var c=o.get(n);if(c)return c==t;e|=2,o.set(n,t);var l=Qu(f(n),f(t),e,u,i,o);
return o.delete(n),l;case E:if(Ur)return Ur.call(n)==Ur.call(t)}return!1}(n,t,c,r,e,i,o);
if(!(1&r)){var v=s&&Cn.call(n,"__wrapped__"),w=h&&Cn.call(t,"__wrapped__");if(v||w)
{var m=v?n.value():n,k=w?t.value():t;return o||(o=new Vr),i(m,k,r,e,o)}}return!!p&&(o||(o=new Vr)

Hard to make any sense out of this, right ?

Need for Source Maps

This is where the magic of source maps come into picture!

Simple speaking source maps are .map files which maps these obfuscated lines of code to the actual lines of code written by the developer.

Why do we need this ? TO DEBUG, duh!

How else are you going to debug issues in that gibberish code ??

Source Maps in Dev tools

You get to view all your beautiful hand-written code inside webpack:// section in dev tools. And you can apply debugger anywhere you want in your actual code. Debugging simplified!

The Problem

If you ship these source maps (.map files) along with your .js files, it would mean anyone can view the actual lines of code written by the developer which many companies consider unsafe.

Yes, it might not necessarily be true for all organizations but definitely the majority feels this way.

So, what to do now ?

My Solution

Many monitoring tools like New Relic do provide an option to upload the source map files so that any bugs or issues on obfuscated production code can be easily debugged.

Another solution is to host the source maps on a host which requires authentication via webpack’s SourceMapDevToolPlugin.

But my solution doesn’t really depend on a host or a monitoring tool.

My solution is a LOCAL SOURCE MAP SERVER which serves .map files to dev tools, which resides only in the developer’s machine, which the developer can in turn use to debug the production issues.

Example

Let’s consider an src/index.js file, which will be bundled and minified by Webpack.

import _ from "lodash";

function component() {
  const element = document.createElement("div");

  // Lodash, now imported by this script
  element.innerHTML = _.join(["Hello", "webpack"], " ");

  return element;
}

document.body.appendChild(component());

In order to specify the name and location of the bundled file, we can use a webpack.config.js file.

const path = require("path");

module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
  },
};

And now, once we call npx webpack --config webpack.config.js, it will create a bundled main.js file inside dist directory.

Now, since this main.js file is obfuscated/minified, let’s add source maps to it, so that it starts making some sense.

We can do this with the help of SourceMapDevToolPlugin provided by Webpack.

const path = require("path");
const { SourceMapDevToolPlugin } = require("webpack");

module.exports = {
  entry: "./src/index.js",
  devtool: false,
  plugins: [
    new SourceMapDevToolPlugin({
      filename: "maps/[file].map",
    }),
  ],
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
  },
};

Rebuild, and the above piece of code will generate a main.js.map file and put it inside dist/maps folder.

Now you and everyone else should be able to debug the minified code of main.js via the actual lines of code mapped with the help of main.js.map file.

Local Source Map Server

With the above config, DevTools knows that it needs to pick up the source map files from dist/maps folder.

So, what if we tell DevTools to look somewhere else ?

We want DevTools to only be able to find source maps in the developer’s machine. So these are the steps that the developer can do -

  1. Create a NodeJS server via ExpressJS.
  2. Make it serve the .map files from dist/maps directory.
  3. Explicitly tell DevTools to find the source maps via our Local Source Map Server.

Presenting… The local source map server - sourceMapServer.js which resides only with developers.

const express = require("express");
const path = require("path");

const app = express();
const PORT = 5050;

// Serve static files from the 'dist/maps' directory
app.use("/maps", express.static(path.join(__dirname, "dist/maps")));

// Default route to check the server
app.get("/", (req, res) => {
  res.send("Source Map Server is running! Access maps at /maps/<filename>.");
});

// Start the server
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

and turn it ON - node sourceMapServer.js

Now, we need to tell DevTools to find the source map of main.js file at http://localhost:5050/maps/main.js.map location.

We can do that by appending a sourceMappingURL via SourceMapDevToolPlugin in webpack.config.js file -

const path = require("path");
const { SourceMapDevToolPlugin } = require("webpack");

module.exports = {
  entry: "./src/index.js",
  devtool: false,
  plugins: [
    new SourceMapDevToolPlugin({
      filename: "maps/[file].map",
      append: "\n//# sourceMappingURL=http://localhost:5050/maps/[file].map",
    }),
  ],
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
  },
};

DONE!!

This entire basic webpack setup is available on my Github.

Conclusion

So, if the above code goes into production, no one from the user base will be able to access source maps. With this setup, only developers who has got the sourceMapServer.js server file ready and running with them, will be able to debug production code via source maps in dev tools.

Yes, it’s a hack, but its a good hack!

Last updated on 20-11-2024