Babel Tutorial Part 3 - Source Maps
Greetings, friends! Another day to learn about Babel. This is Part 3 of my Babel series. If you need to catch up, please look at Part 1 and Part 2. Today, we will be learning about source maps and how to create one with Babel.
What are Source Maps and Why Use Them?
In case you don't know, a source map is a file that maps transformed or transpiled code back to the original source. It allows developers to debug transpiled code in developer tools such as Chrome Developer Tools or Firefox Developer Tools by looking at the original source code including the original line numbers, column numbers, function names, and identifiers that may have been changed during Babel's transpilation step (or any other tool's transpilation process). Below is an example of a source map.
{
"version": 3,
"sources": ["../src/index.js"],
"names": ["Celebrate", "ReactDOM", "render", "document", "getElementById"],
"mappings": "AAAA,MAAMA,SAAS,GAAG,MAAM;AACtB,sBAAO,oFAAP;AACD,CAFD;;AAIAC,QAAQ,CAACC,MAAT,eACE,oBAAC,SAAD,OADF,EAEEC,QAAQ,CAACC,cAAT,CAAwB,MAAxB,CAFF",
"sourcesContent": [
"const Celebrate = () => {\n return <p>It's working! 🎉🎉🎉</p>;\n};\n\nReactDOM.render(\n <Celebrate />,\n document.getElementById('root'),\n);"
]
}
Notice anything interesting about the syntax? It's a JSON object! When you deal with source maps, you may see them named with a js.map
extension. For instance, if I had a file named cool-script.js
, then I would expect that a corresponding source map would be created called cool-script.js.map
. Most IDEs such as VS Code know how to deal with source maps and will automatically identify it as having a JSON-like syntax. If not, you can specify it as JSON, and you should get syntax highlighting.
How to Create Source Maps?
There are many tools out there for creating source maps: Babel, Browserify, Rollup, TypeScript, Uglify, Webpack, and lots of less common npm libraries such as magic-string, one of my favorites 😊. You may notice that multiple libraries and tools, including Babel and Webpack, leverage the popular source-map package, a library created by Mozilla (the foundation who developed Firefox) to create source maps. The source-map library is a bit complicated to use, but Babel and Webpack help abstract out the complexities and provide a simple API and interface for creating source maps for you.
There are two types of source maps: internal (or inline) and external. Inline source maps are simply data URLs that contain the JSON object we saw earlier encoded in base 64. It will look something like this:
// # sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9pbmRleC5qcyJdLCJuYW1lcyI6WyJDZWxlYnJhdGUiLCJSZWFjdERPTSIsInJlbmRlciIsImRvY3VtZW50IiwiZ2V0RWxlbWVudEJ5SWQiXSwibWFwcGluZ3MiOiJBQUFBLE1BQU1BLFNBQVMsR0FBRyxNQUFNO0FBQ3RCLHNCQUFPLG9GQUFQO0FBQ0QsQ0FGRDs7QUFJQUMsUUFBUSxDQUFDQyxNQUFULGVBQ0Usb0JBQUMsU0FBRCxPQURGLEVBRUVDLFFBQVEsQ0FBQ0MsY0FBVCxDQUF3QixNQUF4QixDQUZGIiwic291cmNlc0NvbnRlbnQiOlsiY29uc3QgQ2VsZWJyYXRlID0gKCkgPT4ge1xuICByZXR1cm4gPHA+SXQncyB3b3JraW5nISDwn46J8J+OifCfjok8L3A+O1xufTtcblxuUmVhY3RET00ucmVuZGVyKFxuICA8Q2VsZWJyYXRlIC8+LFxuICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgncm9vdCcpLFxuKTsiXX0=
This line is a special comment placed in your normal .js
JavaScript file that tells your browser that this file contains a source map. If you know anything about data URLs, then you may know that you can simply copy and paste them in the browser's URL bar as if it was a website name. Simply copy the data URL below and enter it in your browser:
data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9pbmRleC5qcyJdLCJuYW1lcyI6WyJDZWxlYnJhdGUiLCJSZWFjdERPTSIsInJlbmRlciIsImRvY3VtZW50IiwiZ2V0RWxlbWVudEJ5SWQiXSwibWFwcGluZ3MiOiJBQUFBLE1BQU1BLFNBQVMsR0FBRyxNQUFNO0FBQ3RCLHNCQUFPLG9GQUFQO0FBQ0QsQ0FGRDs7QUFJQUMsUUFBUSxDQUFDQyxNQUFULGVBQ0Usb0JBQUMsU0FBRCxPQURGLEVBRUVDLFFBQVEsQ0FBQ0MsY0FBVCxDQUF3QixNQUF4QixDQUZGIiwic291cmNlc0NvbnRlbnQiOlsiY29uc3QgQ2VsZWJyYXRlID0gKCkgPT4ge1xuICByZXR1cm4gPHA+SXQncyB3b3JraW5nISDwn46J8J+OifCfjok8L3A+O1xufTtcblxuUmVhY3RET00ucmVuZGVyKFxuICA8Q2VsZWJyYXRlIC8+LFxuICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgncm9vdCcpLFxuKTsiXX0=
Assuming you pasted it correctly and your browser understands how to decode the data URL, then you should see something like this:
See, source maps aren't scary! It's just JSON data! If you format the data nicely, you'll see that it's the same source map example I mentioned at the beginning of this article:
{
"version": 3,
"sources": ["../src/index.js"],
"names": ["Celebrate", "ReactDOM", "render", "document", "getElementById"],
"mappings": "AAAA,MAAMA,SAAS,GAAG,MAAM;AACtB,sBAAO,oFAAP;AACD,CAFD;;AAIAC,QAAQ,CAACC,MAAT,eACE,oBAAC,SAAD,OADF,EAEEC,QAAQ,CAACC,cAAT,CAAwB,MAAxB,CAFF",
"sourcesContent": [
"const Celebrate = () => {\n return <p>It's working! 🎉🎉🎉</p>;\n};\n\nReactDOM.render(\n <Celebrate />,\n document.getElementById('root'),\n);"
]
}
Alright, now that we know what the source map is supposed to look like, let's create an inline source map using Babel. We will continue from where we left off at the end of Part 2. In the previous article, we used Babel to transpile JSX code. We want to create a source map between the original code that contains JSX and the transformed code, which contains the React createElement
methods. It's quite simple! All we do is add a line to our babel.config.json
file:
{
"presets": ["@babel/react"],
"sourceMaps": "inline"
}
Next, we will generate a new build by running npm run build
. This will add an inline source map to our transformed lib/index.js
file:
function Celebrate() {
return /* #__PURE__ */React.createElement('p', null, 'It\'s working! \uD83C\uDF89\uD83C\uDF89\uD83C\uDF89')
}
ReactDOM.render(/* #__PURE__ */React.createElement(Celebrate, null), document.getElementById('root'))
// # sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9pbmRleC5qcyJdLCJuYW1lcyI6WyJDZWxlYnJhdGUiLCJSZWFjdERPTSIsInJlbmRlciIsImRvY3VtZW50IiwiZ2V0RWxlbWVudEJ5SWQiXSwibWFwcGluZ3MiOiJBQUFBLE1BQU1BLFNBQVMsR0FBRyxNQUFNO0FBQ3RCLHNCQUFPLG9GQUFQO0FBQ0QsQ0FGRDs7QUFJQUMsUUFBUSxDQUFDQyxNQUFULGVBQ0Usb0JBQUMsU0FBRCxPQURGLEVBRUVDLFFBQVEsQ0FBQ0MsY0FBVCxDQUF3QixNQUF4QixDQUZGIiwic291cmNlc0NvbnRlbnQiOlsiY29uc3QgQ2VsZWJyYXRlID0gKCkgPT4ge1xuICByZXR1cm4gPHA+SXQncyB3b3JraW5nISDwn46J8J+OifCfjok8L3A+O1xufTtcblxuUmVhY3RET00ucmVuZGVyKFxuICA8Q2VsZWJyYXRlIC8+LFxuICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgncm9vdCcpLFxuKTsiXX0=
Ta-da! 🎉 Super easy! If you want to create an external source map, then you're supposed to enable it by using "sourceMaps": true
option in babel.config.json
, but it appears that this feature is broken 🤔:
{
"presets": ["@babel/react"],
"sourceMaps": true
}
Unfortunately, as of the time of this writing, "sourceMaps": true
and "sourceMaps": "both"
don't generate external source maps like they should! There is an open issue on GitHub about this, but it looks like it's still happening! 😱
Hope is not lost though! 😃 You can use the --source-map
option in the Babel CLI to overwrite the sourceMaps
option in your babel.config.json
file. Let's make a new script called build:external
in our package.json
file:
{
"name": "babel-tutorial-react",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "serve",
"build": "babel src -d lib",
"build:external": "babel src -d lib --source-maps"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/cli": "^7.10.5",
"@babel/core": "^7.11.1",
"@babel/preset-react": "^7.10.4",
"serve": "^11.3.2"
}
}
-s
instead of --source-map
as a shorthand.Now we can generate an external source map using npm run build:external
. This will create a source map named index.js.map
. Your lib/index.js
file will now point to an external source map instead of using a data URL:
function Celebrate() {
return /* #__PURE__ */React.createElement('p', null, 'It\'s working! \uD83C\uDF89\uD83C\uDF89\uD83C\uDF89')
}
ReactDOM.render(/* #__PURE__ */React.createElement(Celebrate, null), document.getElementById('root'))
// # sourceMappingURL=index.js.map
The index.js.map
source map is equivalent to the base 64 decoded version of the data URL we saw earlier when generating an inline source map. When is it good to have an external source map? Some browser monitoring/logging tools such as those created by New Relic and Airbrake allow you to upload source maps to their servers, so you can debug code running in your production environment. It's best practice to minify production-level code to not only obfuscate it (make it hard to reverse-engineer), but also make the file sizes smaller. A smaller file size means your users download the assets faster! By uploading source maps to a secure server, you can hide them from the general public and let your developers debug code in production! If you upload the source maps to New Relic, for instance, their browser monitoring dashboard lets you see the exact line an error occurs in your original code even if your production environment is using transpiled or minified code! 😲
How to Use Source Maps
We talked about what source maps are and how to create them, but how do we use them? Modern browsers usually ship with support for source maps. If you are using Google Chrome, you can easily see the available source maps by clicking on the "Sources" tab in the Chrome dev tools. As seen in the gif below, you can insert a breakpoint in your transpiled code, and Chrome dev tools will automatically map that breakpoint to the original line and column number position of the source file. This makes it very easy to track down bugs in our code, so we can fix them back in our code editor.
The browser looks at the special comment containing the "sourceMappingURL" to determine the location of the source map.
Conclusion
The finished code is located at my GitHub repository. Source maps are a great way to debug code, and Babel makes it super easy to generate them. They provide a link between your original source code and your transformed code. Many tools can generate source maps, not just Babel. You can also generate source maps for CSS, not just JavaScript. I will talk about source maps in great detail in a future article, so look forward to that! In the meantime, please look at the resources I provided to learn more.