question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Default metro.config.js should be configured to support symbol links to packages

See original GitHub issue

Describe the Feature

npm install <path to local package> creates a symbolic link to the package directory. This is useful for monorepos as well as iterating on a package in the context of the consuming app. Unfortunately, the Metro bundler does not support this out of the box (see https://github.com/facebook/metro/issues/1). It is possible though to configure the Metro bundler (via metro.config.js) to make it work with symbolically linked packages. It would be extremely helpful if react-native init MyApp provided a metro.config.js that made symbolically linked packages work by default.

Possible Implementations

There are many work arounds proposed in https://github.com/facebook/metro/issues/1. All of them had problems for me. My solution (used in multiple projects) is to modify metro.config.js to manually follow symbolic links:

/**
 * Metro configuration for React Native
 * https://github.com/facebook/react-native
 *
 * @format
 */

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

// Escape function taken from the MDN documentation: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
function escapeRegExp(string) {
  return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}

// NOTE: The Metro bundler does not support symlinks (see https://github.com/facebook/metro/issues/1), which NPM uses for local packages.
//       To work around this, we supplement the logic to follow symbolic links.

// Create a mapping of package ids to linked directories.
function processModuleSymLinks() {
  const nodeModulesPath = path.resolve(__dirname, 'node_modules');
  let moduleMappings = {};
  let moduleExclusions = [];

  function findPackageDirs(directory) {
    fs.readdirSync(directory).forEach(item => {
      const itemPath = path.resolve(directory, item);
      const itemStat = fs.lstatSync(itemPath);
      if (itemStat.isSymbolicLink()) {
        let linkPath = fs.readlinkSync(itemPath);
        // Sym links are relative in Unix, absolute in Windows.
        if (!path.isAbsolute(linkPath)) {
          linkPath = path.resolve(directory, linkPath);
        }
        const linkStat = fs.statSync(linkPath);
        if (linkStat.isDirectory()) {
          const packagePath = path.resolve(linkPath, "package.json");
          if (fs.existsSync(packagePath)) {
            const packageId = path.relative(nodeModulesPath, itemPath);
            moduleMappings[packageId] = linkPath;

            const packageInfoData = fs.readFileSync(packagePath);
            const packageInfo = JSON.parse(packageInfoData);

            const dependencies = packageInfo.dependencies ? Object.keys(packageInfo.dependencies) : [];
            const peerDependencies = packageInfo.peerDependencies ? Object.keys(packageInfo.peerDependencies) : [];
            const devDependencies = packageInfo.devDependencies ? Object.keys(packageInfo.devDependencies) : [];

            // Exclude dependencies that appear in devDependencies or peerDependencies but not in dependencies. Otherwise,
            // the metro bundler will package those devDependencies/peerDependencies as unintended copies.
            for (const devDependency of devDependencies.concat(peerDependencies).filter(dependency => !dependencies.includes(dependency))) {
              moduleExclusions.push(new RegExp(escapeRegExp(path.join(linkPath, "node_modules", devDependency)) + "\/.*"));
            }
          }
        }
      } else if (itemStat.isDirectory()) {
        findPackageDirs(itemPath);
      }
    });
  }

  findPackageDirs(nodeModulesPath);

  return [moduleMappings, moduleExclusions];
}

const [moduleMappings, moduleExclusions] = processModuleSymLinks();
console.log("Mapping the following sym linked packages:");
console.log(moduleMappings);

module.exports = {
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: false,
      },
    }),
  },

  resolver: {
    // Register an "extra modules proxy" for resolving modules outside of the normal resolution logic.
    extraNodeModules: new Proxy(
        // Provide the set of known local package mappings.
        moduleMappings,
        {
            // Provide a mapper function, which uses the above mappings for associated package ids,
            // otherwise fall back to the standard behavior and just look in the node_modules directory.
            get: (target, name) => name in target ? target[name] : path.join(__dirname, `node_modules/${name}`),
        },
    ),
    blacklistRE: appendExclusions(moduleExclusions),
  },

  projectRoot: path.resolve(__dirname),

  // Also additionally watch all the mapped local directories for changes to support live updates.
  watchFolders: Object.values(moduleMappings),
};

Related Issues

https://github.com/facebook/metro/issues/1

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:12
  • Comments:21 (1 by maintainers)

github_iconTop GitHub Comments

1reaction
mikehardycommented, Sep 8, 2021

This is magic! Thank you @ryantrem - was able to integrate this immediately into an example app from npx react-native init ... where I was locally testing a module I’m developing, “Just Worked™”

1reaction
ryantremcommented, Feb 1, 2021

It looks like you are trying to use this with yarn link? I’ve never tried that, though I would expect it to work. In my case, I have sym links only through npm install of a local package.

What is app_stack.native? Is it some kind of embedded resource? If so, we’ve had problems with resource loading from sym linked modules using this approach, so happy to share what we learn when we finish our in progress investigation around this.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Configuring Metro - Meta Open Source
A Metro config can be created in these three ways (ordered by priority):. metro.config.js; metro.config.json; The metro field in package.json.
Read more >
Metro bundler - Expo Documentation
You can customize the Metro bundler by creating a metro.config.js file at the root of your project. This file should export a Metro...
Read more >
Amplify EAS Metro config migration blockList - Stack Overflow
This worked for configuring the blockList (not the blackListRE ) resolver and load my images appropriately: metro.config.js
Read more >
@expo/metro-config | Yarn - Package Manager
json is NOT supported. Packages using react-native-builder-bob will default to using the CommonJS setting in exotic. If you need to modify your Node...
Read more >
Metro-symlinked-deps - npm.io
Utilities to customize the metro bundler configuration in order to workaround its lack of support for symlinks. The primary use case for this...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found