Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation on using metro bundler #1257

Closed
atrauzzi opened this issue Feb 25, 2019 · 17 comments
Closed

Documentation on using metro bundler #1257

atrauzzi opened this issue Feb 25, 2019 · 17 comments

Comments

@atrauzzi
Copy link

I'm not sure if it's a well supported path or if there are strong arguments for preferring webpack, but I'd love to see more docs and information about using metro.

My goal here would be to leverage consistency of build tooling for my react native vs. react native web projects. But I also know that web projects tend to require a lot of the magic that webpack offers which maybe metro doesn't have?

@necolas
Copy link
Owner

necolas commented Feb 25, 2019

I agree that it will be good once a single bundler can be used for all platforms. And Metro has some nice perf benefits over webpack (laziness). But this is something for the Metro project to eventually document when it is considered stable enough for public consumption. It's not something I can do or promote at this stage. I'd encourage you to reach out to the Metro team if you'd like to discuss this with them, or get pointers to what would be involved!

@necolas necolas closed this as completed Feb 25, 2019
@atrauzzi
Copy link
Author

@necolas - Sounds good, thanks for the info.

So, as of at least the date of our discussion here, best bet is to prefer webpack, eh?

@howlettt
Copy link

howlettt commented Oct 13, 2019

I managed to get Metro bundling react-native-web with the following steps:

Add the following metro.config.js to the root project directory, which is based off the react-native-windows config (https://github.com/microsoft/react-native-windows/blob/3bab85d89f4b01adc59b26a3e523fc4fe6b4ac93/vnext/local-cli/generator-windows/templates/metro.config.js)

const fs = require('fs');
const path = require('path');
const blacklist = require('metro-config/src/defaults/blacklist');

const rnPath = fs.realpathSync(
  path.resolve(require.resolve('react-native/package.json'), '..'),
);
const rnwPath = fs.realpathSync(
  path.resolve(require.resolve('react-native-web/package.json'), '..'),
);

module.exports = {
  resolver: {
    extraNodeModules: {
      // Redirect react-native to react-native-web
      'react-native': rnwPath,
      'react-native-web': rnwPath,
    },
    // Include the macos platform in addition to the defaults because the fork includes macos, but doesn't declare it
    platforms: ['ios', 'android', 'windesktop', 'windows', 'web', 'macos'],
    providesModuleNodeModules: ['react-native-web'],
    // Since there are multiple copies of react-native, we need to ensure that metro only sees one of them
    // This should go in RN 0.61 when haste is removed
    blacklistRE: blacklist([
      new RegExp(
        `${(path.resolve(rnPath) + path.sep).replace(/[/\\\\]/g, '[/\\\\]')}.*`,
      ),

      // This stops "react-native run-web" from causing the metro server to crash if its already running
      new RegExp(
        `${path
          .resolve(__dirname, 'web')
          .replace(/[/\\\\]/g, '[/\\\\]')}.*`,
      ),
    ]),
  },
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: false,
      },
    }),
  },
};

Edit the function getSha1 in node_modules\metro\src\node-haste\DependencyGraph.js to match the following, as described in facebook/metro#330

  getSha1(filename) {
    const resolvedPath = fs.realpathSync(filename);
    const sha1 = this._hasteFS.getSha1(resolvedPath);
    if (!sha1) {
      return getFileHash(resolvedPath)
      function getFileHash(file) {
        return require('crypto')
          .createHash('sha1')
          .update(fs.readFileSync(file))
          .digest('hex')
      }
    }
    return sha1;
  }

You can then create a bundle using the command
react-native bundle --platform web --dev false --entry-file index.js --bundle-output <path-to-output>/web.bundle.js --assets-dest <path-to-output>/assets

Edit:
Apparently I hadn't tested running the android build after these changes. To get other builds working:

  1. Rename the above metro.config.js to metroWeb.config.js
  2. Add --config ../../../../metroWeb.config.js to the react-native bundle command

@petgru
Copy link

petgru commented Nov 17, 2019

@howlettt thanks for sharing, this worked well for me as well :)

I had to add "assetRegistryPath" to the transformer, since some of my libraries include images.

Adding the path

const assetRegistryPath = fs.realpathSync(
    path.resolve(require.resolve('react-native-web/src/modules/AssetRegistry/index.js'), '..'),
)

and changing the transformer

transformer: {
    assetRegistryPath: assetRegistryPath,
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: false,
      },
    }),
  },

In my case I also had to replace "react-native-linear-gradient" with "react-native-web-linear-gradient" using the same method you used to replace "react-native" with "react-native-web".

@opeologist
Copy link

@howlettt does this implementation also allow for Fast Refresh to be used for web?

@howlettt
Copy link

howlettt commented Feb 8, 2020

@tehOPEologist no, you have to manually refresh the page after each change

@opeologist
Copy link

ok, was hoping there's a metro for web solution that uses fast refresh =\

@howlettt
Copy link

howlettt commented Jul 6, 2020

Updated steps for react-native 0.62

Step 1

Add a new file metroWeb.config.js to the root project directory with content:

const fs = require('fs');
const path = require('path');
const blacklist = require('metro-config/src/defaults/blacklist');
const rnPath = fs.realpathSync(
    path.resolve(require.resolve('react-native/package.json'), '..'),
);
const rnwPath = fs.realpathSync(
    path.resolve(require.resolve('react-native-web/package.json'), '..'),
);

module.exports = {
  resolver: {
    extraNodeModules: {
      // Redirect react-native to react-native-web
      'react-native': rnwPath,
      'react-native-web': rnwPath,
    },
    blacklistRE: blacklist([
      // Since there are multiple copies of react-native, we need to ensure that metro only sees one of them
      new RegExp(
        `${(path.resolve(rnPath) + path.sep).replace(/[/\\\\]/g, '\\\\')}.*`,
      ),
      // This stops "react-native run-web" from causing the metro server to crash if its already running
      new RegExp(
        `${path.resolve(__dirname, 'web').replace(/[/\\]/g, '/')}.*`,
      ),
    ]),
    platforms: ['ios', 'android', 'web'],
  },
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: false,
      },
    }),
  },
};

Step 2

Edit the getSha1 function in both the following files to fix facebook/metro#330

[project]\node_modules\metro\src\node-haste\DependencyGraph.js
C:\Users\[User]\AppData\Roaming\npm\node_modules\@react-native-community\cli\node_modules\metro\src\node-haste\DependencyGraph.js

by changing

if (!sha1) {
  throw new ReferenceError(
    `SHA-1 for file ${filename} (${resolvedPath}) is not computed`,
  );
}

to

if (!sha1) {
  return getFileHash(resolvedPath)
  function getFileHash(file) {
    return require('crypto')
      .createHash('sha1')
      .update(fs.readFileSync(file))
      .digest('hex')
  }
}

Step 3

You can then create a bundle using the command:

react-native bundle --platform web --dev false --entry-file index.js --bundle-output [path-to-output]/bundle.js --assets-dest [path-to-output] --config metroWeb.config.js

@mrKorg
Copy link

mrKorg commented Oct 8, 2020

But I don't have this file require('metro-config/src/defaults/blacklist')

@howlettt
Copy link

howlettt commented Oct 9, 2020

@samaneh-kamalian
Copy link

samaneh-kamalian commented Apr 6, 2021

Updated steps for react-native 0.62

Step 1

Add a new file metroWeb.config.js to the root project directory with content:

const fs = require('fs');
const path = require('path');
const blacklist = require('metro-config/src/defaults/blacklist');
const rnPath = fs.realpathSync(
    path.resolve(require.resolve('react-native/package.json'), '..'),
);
const rnwPath = fs.realpathSync(
    path.resolve(require.resolve('react-native-web/package.json'), '..'),
);

module.exports = {
  resolver: {
    extraNodeModules: {
      // Redirect react-native to react-native-web
      'react-native': rnwPath,
      'react-native-web': rnwPath,
    },
    blacklistRE: blacklist([
      // Since there are multiple copies of react-native, we need to ensure that metro only sees one of them
      new RegExp(
        `${(path.resolve(rnPath) + path.sep).replace(/[/\\\\]/g, '\\\\')}.*`,
      ),
      // This stops "react-native run-web" from causing the metro server to crash if its already running
      new RegExp(
        `${path.resolve(__dirname, 'web').replace(/[/\\]/g, '/')}.*`,
      ),
    ]),
    platforms: ['ios', 'android', 'web'],
  },
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: false,
      },
    }),
  },
};

Step 2

Edit the getSha1 function in both the following files to fix facebook/metro#330

[project]\node_modules\metro\src\node-haste\DependencyGraph.js
C:\Users\[User]\AppData\Roaming\npm\node_modules\@react-native-community\cli\node_modules\metro\src\node-haste\DependencyGraph.js

by changing

if (!sha1) {
  throw new ReferenceError(
    `SHA-1 for file ${filename} (${resolvedPath}) is not computed`,
  );
}

to

if (!sha1) {
  return getFileHash(resolvedPath)
  function getFileHash(file) {
    return require('crypto')
      .createHash('sha1')
      .update(fs.readFileSync(file))
      .digest('hex')
  }
}

Step 3

You can then create a bundle using the command:

react-native bundle --platform web --dev false --entry-file index.js --bundle-output [path-to-output]/bundle.js --assets-dest [path-to-output] --config metroWeb.config.js

Hi.
I test this solution but create the bundle command return me this error:
Unable to resolve module ./Libraries/Components/AccessibilityInfo/AccessibilityInfo from node_modules/react-native/index.js:
I see that in this folder(./Libraries/Components/AccessibilityInfo) we just have AccessibilityInfo.android.js and AccessibilityInfo.ios.js .
Is there any solution?

edit:
this is output of npx react-native info:
info Fetching system and libraries information...
System:
OS: macOS 11.1
CPU: (4) x64 Intel(R) Core(TM) i5-5350U CPU @ 1.80GHz
Memory: 57.63 MB / 8.00 GB
Shell: 3.2.57 - /bin/bash
Binaries:
Node: 15.2.1 - /usr/local/bin/node
Yarn: Not Found
npm: 7.4.3 - ~/Desktop/saman-authenticator/node_modules/.bin/npm
Watchman: 4.9.0 - /usr/local/bin/watchman
Managers:
CocoaPods: 1.10.0 - /usr/local/bin/pod
SDKs:
iOS SDK:
Platforms: iOS 14.2, DriverKit 20.0, macOS 11.0, tvOS 14.2, watchOS 7.1
Android SDK:
API Levels: 29
Build Tools: 28.0.3, 29.0.2
System Images: android-29 | Intel x86 Atom_64, android-29 | Google Play Intel x86 Atom
Android NDK: Not Found
IDEs:
Android Studio: 4.1 AI-201.8743.12.41.6953283
Xcode: 12.2/12B45b - /usr/bin/xcodebuild
Languages:
Java: 1.8.0_275 - /usr/bin/javac
Python: 2.7.16 - /usr/bin/python
npmPackages:
@react-native-community/cli: Not Found
react: ^16.13.1 => 16.13.1
react-native: ^0.63.4 => 0.63.4
react-native-macos: Not Found
npmGlobalPackages:
react-native: Not Found

@howlettt
Copy link

@samaneh-kamalian I just tested this locally with react-native 0.64.0 & react-native-web 0.15.7 and it works. The only change I made was replacing
const blacklist = require('metro-config/src/defaults/blacklist');
with
const blacklist = require('metro-config/src/defaults/exclusionList');

Check these links out, they may help
https://microsoft.github.io/react-native-windows/docs/next/metro-config-out-tree-platforms
microsoft/react-native-windows#5965

@samaneh-kamalian
Copy link

Thanks for your reply.
In Diagnosing metro config issues section of this doc, show that there is not any solution for my problem!

@daku
Copy link

daku commented Apr 17, 2023

@samaneh-kamalian I was able to resolve the issue. The problem was that those files should not have been included from react-native inside node_modules. All those imports need to resolve to react-native-web.

The reason this was happening is because of a broken blacklist regular expression in the Metro configuration. It might have been working for others because they are on Windows, but I am on a Mac (just a guess). Here is the new configuration that works for me. Please note I have simply hard-coded the blacklist path, the regular expression still needs to be fixed. I have also shown how to use the bundle and load it from the top-level index.html.

The following applies to a fresh react-native 0.71 app.

metroWeb.config.js:

const fs = require('fs');
const path = require('path');
const blacklist = require('metro-config/src/defaults/exclusionList');
const rnPath = fs.realpathSync(
    path.resolve(require.resolve('react-native/package.json'), '..'),
);
const rnwPath = fs.realpathSync(
    path.resolve(require.resolve('react-native-web/package.json'), '..'),
);

module.exports = {
  resolver: {
    extraNodeModules: {
      // Redirect react-native to react-native-web
      'react-native': rnwPath,
      'react-native-web': rnwPath,
    },
    blacklistRE: blacklist([
      // Since there are multiple copies of react-native, we need to ensure that metro only sees one of them
      // todo: fix this regexp to work generically on multiple platforms
      new RegExp(
        `${'<FULL PATH TO PROJECT>/node_modules/react-native/'}.*`,
      ),
      // This stops "react-native run-web" from causing the metro server to crash if its already running
      new RegExp(
        `${path.resolve(__dirname, 'web').replace(/[/\\]/g, '/')}.*`,
      ),
    ]),
    platforms: ['ios', 'android', 'web'],
  },
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: false,
      },
    }),
  },
};

Rest of the instructions are same as described by @howlettt above. Note the change in blacklist import file name.

How to use the created JS bundle:

  • At the project top level
mkdir public
touch public/index.html
  • Copy the following to public/index.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width">
        <title> Demo Project </title>
    </head>
    <body>
        <div id="root"></div>
        <script src="/index.bundle.js"></script>
    </body>
</html>
  • Remove any references to NewAppScreen from App.tsx. Otherwise, there will be errors while creating the bundle

  • Modify index.js to

/**
 * @format
 */

import {AppRegistry, Platform} from 'react-native';
import App from './App';
import {name as appName} from './app.json';

AppRegistry.registerComponent(appName, () => App);
if (Platform.OS == "web") {
    AppRegistry.runApplication(appName, {rootTag: document.getElementById('root')});
}
  • Make sure the bundle is created with the correct output paths. In this case,
yarn react-native bundle --platform web --dev false --entry-file index.js --bundle-output public/index.bundle.js --assets-dest public/assets --config metroWeb.config.js
  • Finally, run a web server that hosts the contents of your public folder
cd public
npx serve

serve is a simple http server that locally hosts your new react-native-web website.

@darkcake
Copy link

darkcake commented May 7, 2023

Are there some methods to make it working with hot reload?

@gfaraj
Copy link

gfaraj commented Jun 5, 2024

I'm confused -- are these steps still needed today? The Expo docs make it sound really simple to add the web platform, but it doesn't really work.

@Sergio1C
Copy link

Sergio1C commented Dec 7, 2024

Hello. I am, trying to setting up my already existing react-native project (without Expo) with metro bundler but getting the following:

> PS C:\Users\nofxs\source\repos\MyAppChat> npx react-native bundle --platform web --dev false --entry-file index.js --bundle-output web/bundle.js --assets-dest web/assets --config metro.config.web.js --verbose
> debug Reading Metro config from C:\Users\nofxs\source\repos\MyAppChat\metro.config.web.js
> error Invalid platform "web" selected.
> info Available platforms are: "ios", "android", "native". If you are trying to bundle for an out-of-tree platform, it may not be installed.
> error Bundling failed.
> Error: Bundling failed
>     at buildBundleWithConfig (C:\Users\nofxs\source\repos\MyAppChat\node_modules\@react-native\community-cli-plugin\dist\commands\bundle\buildBundle.js:65:11)
>     at Object.buildBundle [as func] (C:\Users\nofxs\source\repos\MyAppChat\node_modules\@react-native\community-cli-plugin\dist\commands\bundle\buildBundle.js:42:10)
>     at async Command.handleAction (C:\Users\nofxs\source\repos\MyAppChat\node_modules\@react-native-community\cli\build\index.js:118:9)

My custom config metro.config.web.js looks working and is used, but it seems like I have not installed some additinal packages or something esle.
My package.json:

{
  "name": "MyAppChat",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "android": "react-native run-android --port 8081",
    "android2": "react-native run-android --port 8088",
    "ios": "react-native run-ios",
    "lint": "eslint .",
    "start": "react-native start --experimental-debugger",
    "start2": "react-native start --port 8088 --experimental-debugger",
    "test": "jest"
  },
  "dependencies": {
    "react": "18.2.0",
    "react-dom": "^18.2.0",
    "react-native": "0.74.6",
    "react-native-web": "^0.19.13",
    "react-native-webrtc": "124.0.4"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0",
    "@babel/preset-env": "^7.20.0",
    "@babel/runtime": "^7.20.0",
    "@react-native/babel-preset": "0.74.88",
    "@react-native/eslint-config": "0.74.88",
    "@react-native/metro-config": "0.74.88",
    "@react-native/typescript-config": "0.74.88",
    "@types/react": "^18.2.6",
    "@types/react-test-renderer": "^18.0.0",
    "babel-jest": "^29.6.3",
    "eslint": "^8.19.0",
    "jest": "^29.6.3",
    "metro-config": "^0.81.0",
    "prettier": "2.8.8",
    "react-test-renderer": "18.2.0",
    "typescript": "5.0.4"
  },
  "engines": {
    "node": ">=18"
  }
}
   

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests