Skip to content

Metro Guide

kmelmon edited this page Jun 9, 2020 · 32 revisions
  1. Overview

Overview

In this article we will cover basics of how the Metro bundler works, how we have customized Metro for react-native-windows, and some internal details to help you find your way around.

Also see: https://github.com/microsoft/react-native-windows/wiki/Metro-Troubleshooting-Guide

The Basics

What is Metro and what are bundles?

Metro is a collection of tightly integrated npm packages, installed along with react-native. See: https://facebook.github.io/metro/. Although the docs are very light on details, I suggest reading them over if you plan on doing any development in the RNW repo. Start here: https://facebook.github.io/metro/docs/concepts

Metro is a node.js application and is invoked by the react-native CLI.

Metro produces javascript "bundles". What is a bundle? Simply put, a bundle is just a large file containing the javascript code required to run the javascript portion of your react-native application, and assets needed by the javascript (eg images).

Metro and apps work with bundles in several ways:

1) Working with Metro/bundles during app development
A bundle can be served up by Metro at runtime to the client app via an http request and the client code will pick up this bundle and run the javascript automatically. This is the normal scenario during app development. This creates a nice separation between the developer box and the test machine, which might be a mobile device. Metro runs an http server listening on localhost:8081 by default, but this can be configured for more advanced scenarios such as remote debugging in a VM (see https://github.com/microsoft/react-native-windows/wiki/VS-Remote-Debugging).

Serving up bundles is also part of how fast refresh does its magic. When the javascript changes, Metro can produce a "patch" to the bundle and sends the patch to the app via another http request. The client code is capable of applying this patch to its current bundle and reloading, including what changed, giving you that "fast refresh" experience - your app can keep going with changes, without a full re-launch.

Of course, for Metro to serve up bundles, you need to first start the Metro server. This can be done a couple ways:
react-native run-windows: This starts Metro and also builds/launches your app
yarn start: This just starts Metro

TBD: Mention the developer menu which has a Debug JS option

Note that Metro is actually launched by the react-native CLI, which is a different npm package maintained by the react-native community. See: https://github.com/react-native-community/cli.
The start command has parameters that allow you to customize the server behavior. For more details see: https://github.com/react-native-community/cli/blob/master/docs/commands.md#bundle

When the client wants a bundle in this mode it makes an http request with a "bundle URL", which is just an http request with query parameters. A typical bundle URL looks something like this:
http://localhost:8081/index.bundle?platform=windows&dev=true&hot=false&inlineSourceMap=true
Unfortunately the query parameters don't seem to be officially documented. Here's what the parameters mean:
platform: the native platform to build a bundle for (eg android/ios/windows)
dev: if true, turns on "dev" mode in the javascript code (adds debugging stuff to the JS)
hot: if true, turns on hot reload (?) which we don't actually use in react-native-windows (we use fast refresh)
inlineSourceMap: if true, creates a source map inline with the bundle. A source map is like a pdb file, it tells the debugger where in the original source file a javascript instruction in the bundle corresponds to.
minify: if true, minifies the javascript
For more details, see source code here: https://github.com/facebook/metro/blob/7814c2840c49c16788041284fd65df25aa997d8c/packages/metro/src/Server.js#L671

Handy debugging trick: You can enter a bundle URL into your browser to test fetching a bundle, if it's successful you'll see the bundle loaded into your browser as plain-text.

2) Creating offline bundles for a release build
The bundle can be packaged directly into your application as a resource (aka "offline bundle"). In this case Metro produces a bundle as part of building your application, and the bundle is loaded directly by the client code, ie there is no http server. This is the normal scenario for a release build. To produce a bundle, you invoke the CLI with the "bundle" command, which then invokes Metro to produce the bundle. A basic bundle command looks something like this:
npx react-native bundle --platform windows --entry-file index.js --bundle-output windows\myAwesomeApp\Bundle\index.windows.bundle --assets-dest windows\myAwesomeApp\Bundle
For more details on bundle parameters, see: https://github.com/react-native-community/cli/blob/master/docs/commands.md#bundle

Configuring Metro

Metro is highly configurable. For docs on the options, see: https://facebook.github.io/metro/docs/configuration

Metro has a default configuration, unfortunately it's not documented, but see: https://github.com/react-native-community/cli/blob/db6dc54479479c9f656bd6d777174223da9901f4/packages/cli/src/tools/loadMetroConfig.ts#L57

Apps can provide a custom metro config that typically adds to the default configuration. For react-native-windows aps created by the CLI we provide a metro.config.js, see: https://github.com/microsoft/react-native-windows/blob/19c35585d90df557ef6d4947daaee0a15cecc7be/vnext/local-cli/generator-windows/templates/metro.config.js#L7

It's worth calling out one of the most commonly used Resolver options, blacklistRE. This option tells the Metro Resolver which files to ignore by putting them on a "blacklist". Once blacklisted, the files are not seen at all by Metro, which can resolve a number of common bundling issues (see https://github.com/microsoft/react-native-windows/wiki/Metro-Troubleshooting-Guide). The property is an array of regular expressions to allow for things like the * wildcard character. See more on how the Resolver works in "Going Deeper" section below.

Debugging the JS

Since react-native apps are largely javascript apps, developers often need to debug the javascript. There are actually a lot of debugging tools out there for react-native, but this article will cover two basic ways of debugging just the javascript:

1) Web Debugging With web debugging, the javascript bundle is not actually running in the client process. Instead it's running in another process (either a web browser, or a tool like VS Code). In order to use web debugging, you need to first start Metro and launch your app in debug mode (see section above called "Working with Metro/bundles during app development"). From there we'll cover two options for debugging:

Debugging using Chrome
With this option, you use the Chrome debugging tools just like you would if you were debugging a web page. When Metro launches, you should see Chrome launch a new tab that navigates to this URL:
http://localhost:8081/debugger-ui/
If you don't see this launch automatically, you can launch it yourself and manually enter that URL. From there you can hit F12 and use the Chrome debugging tools.

Debugging using VS Code
With this option, you can do your debugging directly in VS Code, which is very convenient, especially if you're writing your JS in VS Code. To get started, you'll need to install the react-native tools for VS Code. See:
https://marketplace.visualstudio.com/items?itemName=msjsdiag.vscode-react-native
From there, follow this guide: https://github.com/microsoft/react-native-windows/wiki/VS-Code-Debugging

2) Direct Debugging

Going in Deeper

Customization of metro configuration for react-native-windows

To understand how we customize the metro configuration for react-native-windows, you must first understand an important detail of out-of-tree platforms: platform-specific javascript overrides.

What is an override and how does it work?
react-native-windows is built on top of react-native and shares most of the react-native javascript code. However, some of this code was written with only android/ios in mind and won't work on windows without some changes. There are also cases where windows has functionality that hasn't made it back upstream yet. This is where platform overrides come in. It's expected that the out-of-tree platform may have to override portions of the javascript, this is done simply by making a platform-specific override file for a given javascript file. Metro will use the override in place of the original file when creating bundles. The overridden file has a naming convention of foo.platform.js, where platform is the name of the out-of-tree platform (eg windows). For example, we override Alert.js with Alert.windows.js.

Now for the fun part - getting Metro to pick up the platform override when creating a bundle. There are two parts to this:

  1. When a bundle is being requested, the platform is supplied as a parameter to Metro. This is how Metro knows what overrides to use. Using Alert.js as an example, if the app uses the Alert module, and a bundle is requested with platform = windows, Metro will bundle Alert.windows.js instead of Alert.js.
  2. The override file must be located in the same directory as the file it is overriding. This requirement makes life simpler for the resolver. However this requirement has a very important implication for react-native-windows. The windows overrides don't get published to the react-native npm registry, as they aren't part of react-native. Thus they wont' be present in node_modules/react-native when an app installs react-native. So how do we get windows overrides installed to the same directory? The answer is we publish a copy of all of the react-native javascript along with react-native-windows. This requires some special steps during the build to copy all of the appropriate JS to a single directory before publishing. This is done by copyRNLibraries.js, see: https://github.com/microsoft/react-native-windows/blob/ae37f8e7518f808116ce906a7f7b32f00412612d/vnext/Scripts/copyRNLibraries.js#L79

To give you an idea of the full ramifications of this, here's a picture of the directory structure for a sample "myAwesomeAppp' generated by the CLI, with react-native-windows installed:

myAwesomeApp

index.js App.js node_modules

react-native

(all of react-native, including JS, ios/android native code)

react-native-windows

(all of react-native (JS only), plus windows overrides)

The important thing to notice is that all of the react-native javascript is actually installed to two locations. When you build a bundle for windows, Metro will pick up all of react-native from within node_modules\react-native-windows (along with the windows overrides), NOT from within node_modules\react-native. However, if you build a bundle for ios/android, Metro will fallback to the default behavior of picking up react-native from node_modules\react-native. You might be wondering how this magic works! If so read on.

Metro resolver magic
Because of the custom install configuration described above, we changed Metro to magically redirect files that normally live in node_modules\react-native over to node_modules\react-native-windows. Note that this change will work for other out-of-tree platforms as well. The jist of the change: Metro has a central function for resolving every file in a bundle, and this can be overridden. We introduced a custom function to take over the resolving. When resolving files for a given out-of-tree platform, anything that begins with 'react-native' is redirected to that out-of-tree platform. See: https://github.com/react-native-community/cli/pull/1115

Resolver Internals

The Metro resolver has complex rules, and is where most bundling errors happen. This section covers some of the internal details of the resolver.

What files are being resolved?
It may not be apparent how Metro determines what files to include in a bundle. The short answer is Metro has smarts to pull in only the javascript and assets used by your app code. From the docs:

"Metro needs to build a graph of all the modules that are required from the entry point. To find which file is required from another file Metro uses a resolver. In reality this stage happens in parallel with the transformation stage."

It does this by generating a dependency graph, starting with the entry point to the app (typically, index.js in your app directory)

require/import details
inline requires
relative requires

Haste map