A beginner’s guide to Metro Bundler
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.
- 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 writeout
— The file name and location thatmetro
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
toentry.js
, and worst of all, a single line ofconsole.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!