Skip to content

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

Closed
@ryantrem

Description

@ryantrem

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 facebook/metro#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 facebook/metro#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

facebook/metro#1

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions