Default metro.config.js should be configured to support symbol links to packages
See original GitHub issueDescribe 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
Issue Analytics
- State:
- Created 3 years ago
- Reactions:12
- Comments:21 (1 by maintainers)
Top GitHub Comments
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™”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 throughnpm 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.