A beginner’s guide to Metro Bundler

Isaac Lem
Geek Culture
Published in
6 min readAug 23, 2021
Photo by Markus Spiske on Unsplash

If you have the experience on developing some projects using React Native, chances are you’ve been exposed to the term metro bundler as it is the de facto bundler to bundle your React Native codes which allows your code to work nicely with Native mobile apps.

Everything working great out of the box without needing any configuration (kudos to RN team) but have you ever wondered if we can use metro for non React Native apps?

What is a bundler?

Before we try to understand how metro works, maybe it’s worth to align on the concept of bundler and how it is useful.

On 10,000 foot views, bundler is essentially a system that packs all the different JavaScript files that you wrote and produce a single JS file which you can then use to run your JavaScript application. Generally, bundler builds a dependency graph to maintain the relationships of the different JavaScript files, which eventually spits out one or more JS bundle.

Bundler is especially useful in modern JavaScript development as modern JavaScript relies heavily in module system, which is simply means that instead of writing a big giant JS file, we break them into a smaller, manageable, and modular JS files. Some example of module system are CommonJS, AMD, RequireJS and ES(ECMAScript)6 Modules.

Now that we know the purpose of bundler , what are some of the bundler available in the market? Well, turns out there are actually plenty of choices at your disposal, such as rollup, webpack, parcel, and of course our main highlight today, metro.

Step by step tutorial for metro

As mentioned earlier, we are gonna explore metro to see how we can use it in a non react-native environment in order to understand a little better about metro. Before we proceed, I sure hope that you have some pre-requisite knowledges about some of the basic web development. Fret not! I will try to be as detailed as possible.

  1. Let’s create an empty repository and initialised it to be a NPM project
mkdir my-app && cd my-app && npm init -y

Once you executed the command above, package.json has been generated and we are good to proceed to second step.

2. Install the dependencies required to setup metro

npm install --save-dev metro metro-core

These are the only two dependencies needed for setting up metro.

3. Create a new file with any name that you want, here I will name it build.js , and paste the below content to the file

const Metro = require('metro');Metro.loadConfig().then(async config => {
Metro.runBuild(config, {
entry: './src/entry.js',
out: './dist/entry.js'
});
});

Essentially, we are saying that when we run build.js, I would like to load metro to create a bundle. We have provided two options

  • entry — The JavaScript source file that we are going to write
  • out — The file name and location that metro should generated to

Believe it or not, that’s all you need to do to setup metro! All the remaining steps are just about writing some JavaScript files and to see the result.

Do note that even-though we are using src and dist , they are just normal repo and we are naming it that way simply due to industry standard

4. Create two folders src and dist since we defined it in build.js . This step is optional if you did not specified ./src and ./dist in your build.js

mkdir src && mkdir dist

5. Create entry.js in src folder and simply put some logging as below

console.log('It works!')

6. Execute the command. Please ensure that you are at the root folder when you are executing the below command

node build.js

If you’re seeing the result above, we can then proceed to take a look at the file that created by metro . Proceed to open the file entry.js in dist folder, you should be seeing something like below

var __BUNDLE_START_TIME__=this.nativePerformanceNow?nativePerformanceNow():Date.now(),__DEV__=false,process=this.process||{},__METRO_GLOBAL_PREFIX__='';process.env=process.env||{};process.env.NODE_ENV=process.env.NODE_ENV||"production";!(function(e){"use strict";e.__r=i,e[`${__METRO_GLOBAL_PREFIX__}__d`]=function(e,n,o){if(null!=t[n])return;const i={dependencyMap:o,factory:e,hasError:!1,importedAll:r,importedDefault:r,isInitialized:!1,publicModule:{exports:{}}};t[n]=i},e.__c=o,e.__registerSegment=function(e,r,n){p[e]=r,n&&n.forEach(r=>{t[r]||h.has(r)||h.set(r,e)})};var t=o();const r={},{hasOwnProperty:n}={};function o(){return t=Object.create(null)}function i(e){const r=e,n=t[r];return n&&n.isInitialized?n.publicModule.exports:d(r,n)}function l(e){const n=e;if(t[n]&&t[n].importedDefault!==r)return t[n].importedDefault;const o=i(n),l=o&&o.__esModule?o.default:o;return t[n].importedDefault=l}function u(e){const o=e;if(t[o]&&t[o].importedAll!==r)return t[o].importedAll;const l=i(o);let u;if(l&&l.__esModule)u=l;else{if(u={},l)for(const e in l)n.call(l,e)&&(u[e]=l[e]);u.default=l}return t[o].importedAll=u}i.importDefault=l,i.importAll=u;let c=!1;function d(t,r){if(!c&&e.ErrorUtils){let n;c=!0;try{n=_(t,r)}catch(t){e.ErrorUtils.reportFatalError(t)}return c=!1,n}return _(t,r)}const s=16,a=65535;function f(e){return{segmentId:e>>>s,localId:e&a}}i.unpackModuleId=f,i.packModuleId=function(e){return(e.segmentId<<s)+e.localId};const p=[],h=new Map;function _(r,n){if(!n&&p.length>0){var o;const e=null!==(o=h.get(r))&&void 0!==o?o:0,i=p[e];null!=i&&(i(r),n=t[r],h.delete(r))}const c=e.nativeRequire;if(!n&&c){const{segmentId:e,localId:o}=f(r);c(o,e),n=t[r]}if(!n)throw m(r);if(n.hasError)throw g(r,n.error);n.isInitialized=!0;const{factory:d,dependencyMap:s}=n;try{const t=n.publicModule;return t.id=r,d(e,i,l,u,t,t.exports,s),n.factory=void 0,n.dependencyMap=void 0,t.exports}catch(e){throw n.hasError=!0,n.error=e,n.isInitialized=!1,n.publicModule.exports=void 0,e}}function m(e){return Error('Requiring unknown module "'+e+'".')}function g(e,t){return Error('Requiring module "'+e+'", which threw an exception: '+t)}})('undefined'!=typeof globalThis?globalThis:'undefined'!=typeof global?global:'undefined'!=typeof window?window:this);__d(function(g,r,i,a,m,e,d){"use strict";console.log('It works!')},0,[]);__r(0);

That’s it! We have successfully use metro to “bundle” our JavaScript code successfully. But wait, you might have some questions in mind such as

  • We are just tranforming entry.js to entry.js , and worst of all, a single line of console.log turned into a bunch of gibberish code? How is that useful?
  • How should I verify if the code above actually works?

First of all let’s verify if the generated entry.js actually works. To do this, let’s create a new file index.html and load entry.js into it.

<!DOCTYPE html>
<html lang="en">
<head>
<script src="./dist/entry.js"></script>
</head>
</html>

Once we have this index.html , we can then load the file in any browser, and if you opened developer console , you should be able to see It works! printed in the console, that simply means that the script generated by metro is successfully loaded to browser and produced the expected output.

Now that we have verify that entry.js that metro generated definitely works, we can then try to explore more about having multiple JS files. Head back to src folder and create another file constant.js and paste the below content

module.exports = {
name: 'Isaac'
}

Once it’s done, head back to /src/entry.js and paste the below content

const { name } = require("./constant");console.log(name);

What we have done was actually quite straight forward, instead of just logging a simple It works! message, we want to log a name variable, where the value of this name variable is being maintained in another file constant.js.

Once it’s done, we can then generate the bundle again by running command node build.js in the command prompt. Once it’s done, head back to browser and refresh the page, you should be able to see Isaac printed in developer console.

With this example, hope that you understand point which I’ve mentioned about the purpose of bundler, which is to allowing developer to write multiple files, in our case entry.js and constant.js, and pack into a single JS file entry.js.

If we try to understand dist/entry.js and see what has changed this time

var __BUNDLE_START_TIME__=this.nativePerformanceNow?nativePerformanceNow():Date.now(),__DEV__=false,process=this.process||{},__METRO_GLOBAL_PREFIX__='';process.env=process.env||{};process.env.NODE_ENV=process.env.NODE_ENV||"production";!(function(e){"use strict";e.__r=i,e[`${__METRO_GLOBAL_PREFIX__}__d`]=function(e,n,o){if(null!=t[n])return;const i={dependencyMap:o,factory:e,hasError:!1,importedAll:r,importedDefault:r,isInitialized:!1,publicModule:{exports:{}}};t[n]=i},e.__c=o,e.__registerSegment=function(e,r,n){p[e]=r,n&&n.forEach(r=>{t[r]||h.has(r)||h.set(r,e)})};var t=o();const r={},{hasOwnProperty:n}={};function o(){return t=Object.create(null)}function i(e){const r=e,n=t[r];return n&&n.isInitialized?n.publicModule.exports:d(r,n)}function l(e){const n=e;if(t[n]&&t[n].importedDefault!==r)return t[n].importedDefault;const o=i(n),l=o&&o.__esModule?o.default:o;return t[n].importedDefault=l}function u(e){const o=e;if(t[o]&&t[o].importedAll!==r)return t[o].importedAll;const l=i(o);let u;if(l&&l.__esModule)u=l;else{if(u={},l)for(const e in l)n.call(l,e)&&(u[e]=l[e]);u.default=l}return t[o].importedAll=u}i.importDefault=l,i.importAll=u;let c=!1;function d(t,r){if(!c&&e.ErrorUtils){let n;c=!0;try{n=_(t,r)}catch(t){e.ErrorUtils.reportFatalError(t)}return c=!1,n}return _(t,r)}const s=16,a=65535;function f(e){return{segmentId:e>>>s,localId:e&a}}i.unpackModuleId=f,i.packModuleId=function(e){return(e.segmentId<<s)+e.localId};const p=[],h=new Map;function _(r,n){if(!n&&p.length>0){var o;const e=null!==(o=h.get(r))&&void 0!==o?o:0,i=p[e];null!=i&&(i(r),n=t[r],h.delete(r))}const c=e.nativeRequire;if(!n&&c){const{segmentId:e,localId:o}=f(r);c(o,e),n=t[r]}if(!n)throw m(r);if(n.hasError)throw g(r,n.error);n.isInitialized=!0;const{factory:d,dependencyMap:s}=n;try{const t=n.publicModule;return t.id=r,d(e,i,l,u,t,t.exports,s),n.factory=void 0,n.dependencyMap=void 0,t.exports}catch(e){throw n.hasError=!0,n.error=e,n.isInitialized=!1,n.publicModule.exports=void 0,e}}function m(e){return Error('Requiring unknown module "'+e+'".')}function g(e,t){return Error('Requiring module "'+e+'", which threw an exception: '+t)}})('undefined'!=typeof globalThis?globalThis:'undefined'!=typeof global?global:'undefined'!=typeof window?window:this);__d(function(g,r,i,a,m,e,d){"use strict";const{name:n}=r(d[0]);console.log(n)},0,[1]);__d(function(g,r,i,a,m,e,d){"use strict";m.exports={name:'Isaac'}},1,[]);__r(0);

You will realized that most of the content of entry.js are still remain the same. A high level understanding of this is that, every file that we have written, will have a corresponding section such as below

  • entry.js computes
__d(function(g,r,i,a,m,e,d){"use strict";const{name:n}=r(d[0]);console.log(n)},0,[1]);
  • constant.js computes
__d(function(g,r,i,a,m,e,d){"use strict";m.exports={name:'Isaac'}},1,[]);

These are the `dependencies graph` that I’ve previously mentioned, which is how most bundler actually bundles all your JS files into a single JS file and still works!

That’s all I wanted to share in this story, thank you for reading and hopefully it helps you in certain way! For feedback and collaboration opportunities, I invite you to connect with me and let’s keep this conversation going!

--

--

Isaac Lem
Geek Culture

Software Engineer | Keyboard Enthusiast | Coffee-lover