Do you want to request a feature or report a bug?
Feature
What is the current behavior?
I'm removing a test from DependencyGraph-test that looked like so, but wasn't actually working after I fixed problems with the fs mocks:
it('should work with packages with symlinked subdirs', function() {
var root = '/root';
setMockFileSystem({
'symlinkedPackage': {
'package.json': JSON.stringify({
name: 'aPackage',
main: 'main.js',
}),
'main.js': 'lol',
'subdir': {
'lolynot.js': 'lolynot',
},
},
'root': {
'index.js': [
'/**',
' * @providesModule index',
' */',
'require("aPackage/subdir/lolynot")',
].join('\n'),
'aPackage': { SYMLINK: '/symlinkedPackage' },
},
});
var dgraph = new DependencyGraph({
...defaults,
roots: [root],
});
return getOrderedDependenciesAsJSON(dgraph, '/root/index.js').then(function(deps) {
expect(deps)
.toEqual([
{
id: 'index',
path: '/root/index.js',
dependencies: ['aPackage/subdir/lolynot'],
isAsset: false,
isJSON: false,
isPolyfill: false,
resolution: undefined,
resolveDependency: undefined,
},
{
id: 'aPackage/subdir/lolynot.js',
path: '/root/aPackage/subdir/lolynot.js',
dependencies: [],
isAsset: false,
isJSON: false,
isPolyfill: false,
resolution: undefined,
resolveDependency: undefined,
},
]);
});
});
What is the expected behavior?
Reintroduce the test and verify symlinks work.
symlinks not working in React Native is one of the biggest RN complaints, so if my understanding is correct and this would fix that, this would be great!
I think we won't have time to deal with this anytime soon unfortunately. I'd love to reopen once we have a strong case.
Can we leave this open until it's resolved?
This is a huge PITA for code sharing, and developing modules. I'm not sure what your looking for in terms of a strong case, but it's definitely a major pain point for me, and module developers.
Currently I am trying to use lerna, and react-primitives to reorganize a project boilerplate to maximize code reuse, while maintaining upgradability by rn.
Lerna works by symlinking and installing your packages in a mono repo, which is an incredibly helpful pattern for code reuse, and completely broken when the react native packager won't follow symlinks.
Yeah, it should be open. We should find a fix but we are not actively invested in making this change ourselves right now. If the community can find a reasonable solution, we should merge it and make it work.
I suspect it has to do with this line
according to the docs you may also need to check if it's a symlink. EG
//...
this._moduleResolver = new ModuleResolver({
dirExists: filePath => {
try {
var stats = fs.lstatSync(filePath);
return stats.isDirectory() || stats.isSymbolicLink();
} catch (e) {}
return false;
},
// ...
Though I am short on time for digesting how to get started with this project at the moment. I may have more time over the weekend for a proper PR.
Or if someone else gets the chance to play around with it, that would be awesome.
I never have enough time to accomplish all the things I want to do :(
Edit: added "may", as the docs don't explicitly say you isDirectory() won't return true on a symlink, but I believe that is the case.
Edit^2: https://github.com/ericwooley/symlinkTest You do need to explicitly check on OSX Sierra at least, not sure about linux or windows.
Edit^3: the commit where the test was removed https://github.com/facebook/metro-bundler/commit/c1a81571fca6c5256546cdcb7c6be3ef9bdc0588
I'd love to reopen once we have a strong case.
With npm 5, all local installs (npm i ../foo) are now symlinked. This makes it pretty rough to work with projects that have local dependencies. (Thatβs on top of the monorepo scenario.)
Edit^3: the commit where the test was removed c1a8157
The test was removed because it was already broken for a long time, unfortunately (I forgot to put that in the changeset description apparently). Now I realised I shouldn't have removed it completely, only make verify the broken behavior.
I suspect it has to do with this line
dirExists is only used for debugging, not for resolution. The resolution is done based on the HasteFS instance provided by the jest-haste-map module. I believe, but may well be wrong, that a fix for symlinks will have to be implemented in jest-haste-map, both for its watchman and plain crawlers, as well for its watchman and plain watch modes. Since, last time I've heard (might have changed!), watchman doesn't handle symlink, we were hampered in these efforts.
dirExists is only used for debugging, not for resolution. The resolution is done based on the HasteFS instance provided by the jest-haste-map module. I believe, but may well be wrong, that a fix for symlinks will have to be implemented in jest-haste-map, both for its watchman and plain crawlers, as well for its watchman and plain watch modes. Since, last time I've heard (might have changed!), watchman doesn't handle symlink, we were hampered in these efforts.
Interesting.
I am pretty confused by this project, as I am not sure how it is imported by RN. But I manually edited this change into node_modules/react-native/packager/src/node-haste/dependencyGraph.js to use my suggested code and no longer got errors about symlinked modules not being found. I got a ton of other errors, i think related to multiple installs of react-native, but I seemed to have gotten much further in the process.
I also put a console log there to offer some insight, and it was definitely logging. So that line appears to be used by the react-native project while doing real work (as opposed to debugging).
After much frustration with this issue, it ended up being easier to PR the libs i need to work with haul, rather than try to tackle this problem. I wish I had more time for this issue, but it appears to be a pretty rough one to solve, given what @jeanlauliac said.
Hopefully this gets officially resolved soon. as I would much prefer to use the built in solution.
On a side note, haul seems to have fixed all the symlink issues. Is there a downside to haul that you know of? It seems like it could be something that could be officially brought into the fold.
Something that half-works is not symbolic linking but hard linking. You can't hard link directories, but you can use cp to copy all the directories and hard link every file rather than copy it with -al, e.g. cp -al <sourceDirectory> <destinationDirectory>.
The only catch is that it seems in order to trigger rebundling I have to save the changed file (even though the action of saving isn't changing the contents of the file).
@ericwooley Can you explain how your PR, haul, and storybooks (?) relate to the react-native symbolic link issue?
@bleighb
Haul is an alternative to the react-native packager that doesn't have issues with symlinks. They reference this issue because those PR's and issues are about issues with symlinking and the metro bundler.
As to your cp -r issue, you might have better luck with rsync. Which is something I considered at one point.
I'm in the lerna boat, too. The way I finally worked around this issue was to just explicitly add the packages in our monorepo to rn-cli.config.js, instead of having them as dependencies in the RN project's package.json (and symlinked into node_modules)
var config = {
getProjectRoots() {
return [
path.resolve(__dirname),
path.resolve(__dirname, '../some-other-package-in-lerna-monorepo')
]
}
}
May have changed for Metro. HTH
@kschzt I will have to try that solution tonight.
I imagine that there will be more pressure on this issue once yarn workspaces are released. Until then, your solution may be the only way forward.
I was unable to get things to work in something like a monorepo when trying @kschztβs approach. If I have module A as a dependency and point rn-cli.config.js to it explicitly, then it will not be found by packages requiring it from inside the βrootβ repo. In fact, the packager doesnβt even look! The structure is probably fairly obvious from a log excerpt:
Looking for JS files in
/mnt/scratch/shared/petter/owl-devenv/client/root/local
/mnt/scratch/shared/petter/owl-devenv/client/root/local/@client
/mnt/scratch/shared/petter/owl-devenv/client/root/local/@client/util
/mnt/scratch/shared/petter/owl-devenv/client/root
React packager ready.
Loading dependency graph, done.
warning: the transform cache was reset.
error: bundling: UnableToResolveError: Unable to resolve module `@client/util/memoizeOnce` from `/mnt/scratch/shared/petter/owl-devenv/client/root/node_modules/@client/store/bind.js`: Module does not exist in the module map or in these directories:
/mnt/scratch/shared/petter/owl-devenv/client/root/node_modules/@client/util
Why does it not check to see if the module exists inside the local/ directory? Beats me.
(react-native v0.45.0.)
@kschzt @haggholm
How are you importing your files
import whatever from '../../otherPackage/someModules'?import whatever from 'otherPackage/someModules'?import whatever from 'someModules'?I'm not really sure how roots work in this context, but the way your importing may be a factor. Or are you using the @providesModule syntax?
@ericwooley 2(ish): import whatever from '@foo/bar/baz, in this case, with baz having .js, .android.js, and .ios.js versions for different platforms.
Both the project and I are new to React Native (migrating an existing web app); Iβm not familiar with the @providesModule syntax. Perhaps I should look into that and see if it worksβ¦
funny, it used to work (import { something } from 'symlinked-package'), now it doesn't anymore after upgrading react native from 0.43 to 0.45, anything changed in the packager from there?
This lerna project seems to work with Symlinks too (react-native 0.40)
I have played with that too @AshCoolman, and it does seem to work, but setting it up in a similar way with other versions has not worked.
@ericwooley My experience too
Is the only solution at the moment to use haul or some custom shell scripts?
We're trying to use yalc to test packages before we release them, but this deficiency means we have to publish, then test, which is pretty crazy.
In China, 90% local project development use cnpm which is build with npminstall, they are using symlinks to save disk volume. We think not supporting symlinks is not a good idea.
Punting on this issue is making a mess out of the ecosystem. By forcing dependencies on alternative builders for reasonably common setups, we end up stuck on brittle build stacks that are not tested as part of the official release pipeline. Upgrades then become dangerous.
^ Looks like there's progress being made over in jest on fixing the underlying issue by exposing a parameter to allow passing on watchman and following symlinks. If/when that's resolved, it may be possible to parameterize metro-bundler with something that passes through.
I would be very happy if someone would like to take over this task and takes the time to investigate and implement the necessary fixes, both in dependencies and in metro-bundler. Let's make sure efforts are not duplicated. We will not have bandwidth to implement this ourselves in the mid future I'm afraid, but I welcome PRs for review.
I can't use react-native 0.48 because I have to use haul to fix this issue and haul is not working properly with 0.48 right now... π’
I forked a npm package and want to work on it locally by using yarn link. But it's not possible with react-native because of this issue. Is there a workaround for this?
@macrozone
I belive the problem is two-fold: One is that watchman, the tool in charge of monitoring files and triggering a compile whenever a file changes, doesn't work cross symlinks. So a file change within a symlinked folder will never trigger a re-compile.
The other problem is that React Native doesn't seem to follow symlinks outside the project root, but this you can fix by adding another project root through the react native config.
So first npm link like you did before, and then create a file called rn-cli-config.js, and put it in your root react native folder.
var path = require("path");
var config = {
getProjectRoots() {
return [
// Keep your project directory.
path.resolve(__dirname),
// Include your forked package as a new root.
path.resolve(__dirname, "../../../your-forked-package")
];
}
}
module.exports = config;
Use the config together with your normal react-native command. Eg:
react-native start --config rn-cli.config.js
I believe this is what made it work for me (you just have to restart the bundler for it to recompile the linked package). Tell me if you have any issues, and good luck!
thanks @sverrejoh, will try that out
@sverrejoh Thanks it worked for me.
I am using the following command instead in case your global react-native-cli does not match your project react-native version:
node node_modules/react-native/local-cli/cli.js start --config ../../../../rn-cli-config.js
@sverrejoh This almost works for me. I'm using yarn workspaces, my config looks like:
const config = {
getProjectRoots() {
return [path.resolve(__dirname), path.resolve(__dirname, '..')];
},
};
react-native start runs fine. Attempting to run react-native link breaks down with "Cannot find module". Is there some way I can set the module search paths?
@joearasin Not sure what your problem is, but it is possible to also add modules explicitly with the extraNodeModules setting:
var path = require("path");
var config = {
extraNodeModules: {
react: path.resolve(__dirname, "node_modules/react")
}
}
module.exports = config;
@sverrejoh The specific situation I'm dealing with is that I have a monorepo set up (ideally using yarn workspaces) with a react-native app, a react-web app, and a shared library (that needs to be babelified). I'm trying to figure out how to make the shared library (+ dependencies) accessible to the react-native app.
I've managed to link two react-native projects together with everything (at least react-native start and live reloading) working.
I have a library with common components, using typescript, that is npm linked to my main application
1) npm install on both items
2) npm link on YOUR_RN_LIBRARY
3) npm link YOUR_RN_LIBRARY on the main project
4) create rn-cli.config.js in the main project:
var path = require("path");
const metroBundler = require('metro-bundler');
var config = {
extraNodeModules: {
"react-native": path.resolve(__dirname, "node_modules/react-native"),
},
getProjectRoots() {
return [
// Keep your project directory.
path.resolve(__dirname),
path.resolve(__dirname, "LIBRARY_RELATIVE_PATH"),
];
},
getBlacklistRE: function() {
return metroBundler.createBlacklist([
/USER[/\\]PATH[/\\]TOLIBRARY[/\\]node_modules[/\\]react-native[/\\].*/
]);
}
}
module.exports = config;
It seems like the blacklist must be the absolute path to your library on your file system. also for some reason i have to specify exactly where react-native is with extraNodeModules π
finally. npm link is working.
(note that i got this working on RN 0.47.2 - i think bundler changed createBlacklist in 48.X)
I've been using the above and for regular stuffs it's been working pretty well.
Although I have issues when a dependency of my package is react-native.
As an example, I'm using Lerna to manage a RN project which looks like this:
/packages
/ui
/node_modules
/src
/Spinner.js
/main
/node_modules
/src
/index.js
/rn-cli.config.js
I use import Spinner from 'ui'; in main/src/index.js. Spinner.js uses react-native-spinkit to display a loading indicator. react-native-spinkit tries to load react-native from /ui/node_modules, although because I've black-listed it to avoid the naming collision, it cannot find it. Is there any solution?
Unable to resolve
react-nativefromreact-native-spinkit/index.js[...]. Module does not exist inui/node_modules
@alexmngn do you have "extraNodeModules" specified to your main project? (like i have above)
If you're running react-native start from the main folder, it should be exactly the same path that i have.
@sebirdman I had it for "react" but not for "react-native". Having them both does the job for my config π Thanks
@Alazoral, another way you can test release packages is to do a npm pack which generates the .tgz file which is what is uploaded to npm. Then you can npm install ./path/to/modules.tgz and that should work just fine.
If you update, then you have to npm pack and npm install module.tgz again though.
based on https://github.com/facebook/metro-bundler/issues/1#issuecomment-333658773, I created a script that automated the workarounds with rn-cli-config.js to make npm link work https://gist.github.com/GingerBear/485f922a1e403739dc56d279925b216d
what it does is basically
so the workflow is, do regular npm link first, then run node ./react-native-start-with-link.js to start bundler with symlink
@GingerBear that's gold! Thanks so much for posting that script. I just used it on crna/expo to use external modules too. I'll do a write up before the end of the week on how to go about it there. react-community/create-react-native-app/issues/232
@GingerBear
Nice one!! Just made a slight modification as I wanted it to launch expo using the packager config you created through your script rather than just the packager:
https://gist.github.com/nicolascouvrat/68c546860f8c7b8eaeabda18a9baed6b
In case anyone is stuck with this and until the different issues are fixed, I made a little guide on how to use yarn workspaces with Create React App and Create React Native App (Expo) to share common code across. Hope you find it handy! https://learn.viewsdx.com/how-to-use-yarn-workspaces-with-create-react-app-and-create-react-native-app-expo-to-share-common-ea27bc4bad62~~ https://medium.com/viewsdx/how-to-use-yarn-workspaces-with-create-react-app-and-create-react-native-app-expo-to-share-common-ea27bc4bad62
@GingerBear, thanks for that snippet! I reworked it a bit so it picks up the list of workspaces and published it as a package here https://www.npmjs.com/package/metro-bundler-config-yarn-workspaces
@GingerBear I'm having trouble getting this method to pick up dependencies of the sym linked dependency. Any suggestions or is this not possible?
@adamconservis what's the trouble you had? Originally I was testing with [email protected], and I just created an empty project with [email protected], both are working fine.
I have a React Native project that is working fine, and I would like to be able to develop my own modules that can be reused between this and our React front end. To test this, I took a dependency from the project and cloned it to my machine. Then, I symlinked the dependency into the main project and ran this script, with one change. I had the react-native execute run-ios instead of start and what happens is the system finds the dependency that was symlinked, but it has dependencies that the project can't find. I've tried 3 different things. Running just as explained. Then I tried running yarn on the dependency so it would grab all of it's dependencies, that didn't work. Then I tried adding the necessary dependencies of the dependency into the main project, and that didn't work.
@adamconservis
try not using run-ios but use start, run-ios seems ignore the rn-cli-config that specified by ---config
@GingerBear using start doesn't run the app though, how do I trigger the build?
@GingerBear If I run-ios with the packager that was started by the script, I still run into the same issue. require can't find the child of the dependency.
@adamconservis you can try run react-native run-ios to run the app, then exit the packager that started by run-ios, then run the script (which start the packager again with --config ../../../../rn-cli-config-with-links.js), then ues cmd+r to reload the app.
@GingerBear Unfortunately, this still results in the same issue.
@adamconservis Did you try adding those dependencies under extraModules in your rn-cli.config.js file?
@evrimfeyyaz Yeah, I've tried putting the module in there, it's dependencies in there, and both at the same time, and I still get the same error.
Hello, I spent too much time trying to find workarounds for this issue, I finally got the start command to work, but the bundle one still fails to resolve symlinks.
Can someone create a tasks list with all the fixes that still need to be implemented in order to get symlinks to work ? I want to fix this, but right now I have no idea where to start.
Thanks in advance !
Improving the speed of Metro takes a backseat to this feature. Rewrite the bundler if you need to. Do something hacky. Do something duct tape-y. Because in the end, what matters is the library you ship.
@adamconservis did you ever find a solution, I am having a similar issues with the peerDependencies of my package
need+1
now I use a watch-sync way to share common file:
@adamconservis I figured it out, you just need to pass any peerDependencies to extraNodeModules, as said here: https://github.com/facebook/metro/issues/1#issuecomment-334546083
I find that react-native link does not work with the extraNodeModules feature.
No you need to update the paths that react-native link writes manually to wear the packages actually are. Thereβs no workaround for that but itβs also quite easy to fix.
Sent with GitHawk
@fatfatson I use https://github.com/wix/wml which I think it's similar.
So is syncing file copies the only workaround by now? Thanks in advance
No you just need to setup the custom metro config as mentioned before and if you have native modules in different folders, just adjust the paths to those modules in the ios and android code. Since this doesn't need to be bundled, it's super straight forward to use native modules in different folders. react-native link simply creates the paths for you and is a tiny convenience, just adjust the paths and you are good to go. iOS doesn't support symlinks so that could probably never be fixed.
I've setup everything properly and symlinks are working great using react-native: 0.53.0 in development so first of all thanks @GingerBear for creating an easy startup method.
I managed to link the assets as well but now facing another problem in Android.
The problem start when trying to build an release build in Xcode. First it returns an error main.jsbundle can't be found which is solved with the following command which builds the main.jsbundle manualy
react-native bundle --entry-file ./index.js --platform ios --dev false --bundle-output ios/main.jsbundle --config /absolute/path/to/rn-cli-config-with-links.js
After relinking the main.jsbundle in Xcode the error is gone and build fine but without any assets so i don't have any images or other static files in my release app.
./gradlew assembleRelease returns The name react-native was looked up in the Haste module map
Anyone facing the same issue or have a solution for this specific issue?
please fix this :(
Incase it helps anyone else, I spent a lot of time trying to workaround this problem and ended up switching to Haul instead of Metro. Haul works with symlinks https://github.com/callstack/haul
Got it to work with the following setup. Project structure is just an example, it shows at least the shared package out of the app and web dir.
Project structure:
Shared
create .babelrc with following content:
{
"presets": [
"stage-1"
],
"plugins": [
[
"transform-runtime",
{
"polyfill": false,
"regenerator": true
}
]
]
}
In case needed my package.json looks like this. Don't forget to change the name of the shared project. This is the endpoint npm link will use later on.
package.json ==> https://pastebin.com/raw/bLhYDUP5
Shared is now setup properly so run npm link:
cd ./shared
npm link
When developing just run npm run watch in de shared dir.
App
Create an alternative startup script so don't use react-native run-[platform] anymore. Create a startup script (EG: react-native-start.js) with the following content from pastebin.
react-native-start.js ==> https://pastebin.com/raw/jMv9vpq5
Now link the shared package and add the following line to the package.json:
cd ./app
npm link [package_name]_shared
add to package.json:
"[package_shared]_shared": "file:../shared",
From now on you just can start and use symlinks with react-native. This script creates another script called rn-cli.config.js so metro bundler knows what to blacklist and what to use.
node react-native-start.js
Web
No additional config is needed for web. Just link the npm package and add it to the package.json
link the shared package and add the following line to the package.json:
cd ./web
npm link [package_name]_shared
add to package.json:
"[package_shared]_shared": "file:../shared",
In case this still doesn't work let me know and i will see if i can help you out.
I've tried haul as well as an alternative, but it was still so unstable that it didn't make sense for me, your mileage may vary of course and maybe it's improved much since. However with a custom metro config everything has been working like a charm for me for a while.
The custom script I use to start the bundler is the following. It checks for symlinked packages and then generates a metro.config.js file based on that. It also checks the peer dependencies of those linked packages, making sure that those are available to the symlinked packages. Then the script starts the metro bundler, but you can also simply start the bundler yourself by passing the --config flag in other places, like for building for production, as the config file is written into your directory.
This script is heavily inspirend by the previous work of @GingerBear and @sebirdman in this thread.
/*
- check symlink in depencency and devDepency
- if found, generate rn-cli-config.js
- react-native start with rn-cli-config
Sources:
https://github.com/facebook/metro/issues/1#issuecomment-346502388
https://github.com/facebook/metro/issues/1#issuecomment-334546083
*/
const fs = require('fs');
const exec = require('child_process').execSync;
const getDependencyPath = (dependency) => fs.realpathSync(`node_modules/${dependency}`);
const getSymlinkedDependencies = () => {
const packageJson = require(`${process.cwd()}/package.json`);
const dependencies = [
...Object.keys(packageJson.dependencies),
...Object.keys(packageJson.devDependencies),
];
return dependencies.filter((dependency) =>
fs.lstatSync(`node_modules/${dependency}`).isSymbolicLink()
);
};
const generateMetroConfig = (symlinkedDependencies) => {
const symlinkedDependenciesPaths = symlinkedDependencies.map(getDependencyPath);
const peerDependenciesOfSymlinkedDependencies = symlinkedDependenciesPaths
.map((path) => require(`${path}/package.json`).peerDependencies)
.map((peerDependencies) => (peerDependencies ? Object.keys(peerDependencies) : []))
// flatten the array of arrays
.reduce((flatDependencies, dependencies) => [...flatDependencies, ...dependencies], [])
// filter to make array elements unique
.filter((dependency, i, dependencies) => dependencies.indexOf(dependency) === i);
fs.writeFileSync(
'metro.config.js',
`/* eslint-disable */
const path = require('path');
let blacklist
try {
blacklist = require('metro-bundler/src/blacklist');
} catch(e) {
blacklist = require('metro/src/blacklist');
}
module.exports = {
extraNodeModules: {
${peerDependenciesOfSymlinkedDependencies
.map((name) => `'${name}': path.resolve(__dirname, 'node_modules/${name}')`)
.join(',\n\t\t')}
},
getBlacklistRE() {
return blacklist([
${symlinkedDependenciesPaths
.map(
(path) =>
`/${path.replace(/\//g, '[/\\\\]')}[/\\\\]node_modules[/\\\\]react-native[/\\\\].*/`
)
.join(',\n\t\t\t')}
]);
},
getProjectRoots() {
return [
// Include current package as project root
path.resolve(__dirname),
// Include symlinked packages as project roots
${symlinkedDependenciesPaths.map((path) => `path.resolve('${path}')`).join(',\n\t\t\t')}
];
}
};`
);
};
/* global process */
const symlinkedDependencies = getSymlinkedDependencies();
// eslint-disable-next-line no-console
console.log(`
Detected symlinked packaged:
${symlinkedDependencies
.map((dependency) => ` ${dependency} -> ${getDependencyPath(dependency)}`)
.join('\n')}
`);
generateMetroConfig(symlinkedDependencies, 'metro.config.js');
// eslint-disable-next-line no-console
console.log('Generated custom metro.config.js to support symlinks\n');
const command = process.argv[2];
const flags = process.argv.slice(3).join(' ');
exec(
`node node_modules/react-native/local-cli/cli.js ${command} --config ../../../../metro.config.js ${flags}`,
{ stdio: [0, 1, 2] }
);
Maybe it would be an option for metro to do something like this internally, but someone from the core team would have to comment on that.
@MrLoh your script relies on linked package to be referenced in package.json
Could you shine some light on linked package pre-requirements your script uses in regards to 1. and 2.? Thx
I'm using yarn, so not sure wheter that makes a difference but my linked packages are declared as bla-package: ../black-package and everything works fine.
since yarn does not create a symlink, unlike npm
not sure what you are talking about, yarn link certainly creates symlinks. But yes, you have to execute the linking, you can't simply declar it in your package.json, unless you are using workspaces.
https://yarnpkg.com/lang/en/docs/cli/link/
@MrLoh Script worked great!
It would be nice if metro did something similar internally. As a temporary workaround you could publish that script as an npm module for people to use.
I think the problem @ak99372 is mentioning is that if you had local-package-a which depended on local-package-b then local-package-b would not be picked up by your script.
To fix that the script could instead just loop through all the folders in node_modules instead of looking at package.json.
I made this script available via npm https://github.com/MrLoh/metro-with-symlinks.
Simply yarn add -D metro-with-symlinks and replace the start script in your package.json with node ./node_modules/metro-with-symlinks start. If anyone really needs support for nested symlinked packages, feel free to open a PR or create an issue in my repo.
Thank you @MrLoh I've found this script to be the least intrusive way of getting symlinks to work.
However, instead of replacing my start script with metro-with-symlinks start I've instead called metro-with-symlinks in my projects custom bootstrap command (generating the metro.config.js) and added a rn-cli.config.js that just exports metro.config.js. This has the benefit of not needing to update Xcode and Gradle to point to the config since react native automatically looks for rn-cli.config.js
For everyone else having this problem, this works for me with the newest packager:
master-project/rn-cli.config.js
const path = require('path');
const fs = require('fs');
let blacklist,
getPolyfills;
// Get blacklist factory
try {
blacklist = require('metro-bundler/src/blacklist');
} catch(e) {
blacklist = require('metro/src/blacklist');
}
// Get default react-native polyfills
try {
getPolyfills = require('react-native/rn-get-polyfills');
} catch(e) {
getPolyfills = () => [];
}
// See if project has custom polyfills, if so, include the path to them
try {
let customPolyfills = require.resolve('./polyfills.js');
getPolyfills = (function(originalGetPolyfills) {
return () => originalGetPolyfills().concat(customPolyfills);
})(getPolyfills);
} catch(e) {}
const moduleBlacklist = [
//Add whatever you need to the blacklist for your project
/node_modules[^\/]+\/.*/
];
const baseModulePath = path.resolve(__dirname, 'node_modules');
// NOTE: Scoped modules hasn't been fully tested yet. Please respond to
// let th317erd know if this code works with scoped modules
function getSymlinkedModules() {
function checkModule(fileName, alternateRoots) {
try {
let fullFileName = path.join(baseModulePath, fileName),
stats = fs.lstatSync(fullFileName);
if (stats.isSymbolicLink()) {
let realPath = fs.realpathSync(fullFileName);
if (realPath.substring(0, (__dirname).length) !== __dirname)
alternateRoots.push(realPath);
}
} catch (e) {}
}
function checkAllModules(modulePath, alternateRoots) {
var stats = fs.lstatSync(modulePath);
if (!stats.isDirectory())
return alternateRoots;
fs.readdirSync(modulePath).forEach((fileName) => {
if (fileName.charAt(0) === '.')
return;
if (fileName.charAt(0) !== '@')
checkModule(fileName, alternateRoots);
else
checkAllModules(path.join(modulePath, fileName), alternateRoots);
});
return alternateRoots;
}
return checkAllModules(baseModulePath, []);
}
function getExtraModulesForAlternateRoot(fullPath) {
var packagePath = path.join(fullPath, 'package.json'),
packageJSON = require(packagePath),
alternateModules = [].concat(Object.keys(packageJSON.dependencies || {}), Object.keys(packageJSON.devDependencies || {}), Object.keys(packageJSON.peerDependencies || {})),
extraModules = {};
for (var i = 0, il = alternateModules.length; i < il; i++) {
var moduleName = alternateModules[i];
extraModules[moduleName] = path.join(baseModulePath, moduleName);
}
return extraModules;
}
//alternate roots (outside of project root)
var alternateRoots = getSymlinkedModules(),
//resolve external package dependencies by forcing them to look into path.join(__dirname, "node_modules")
extraNodeModules = alternateRoots.reduce((obj, item) => {
Object.assign(obj, getExtraModulesForAlternateRoot(item));
return obj;
}, {});
if (alternateRoots && alternateRoots.length)
console.log('Found alternate project roots: ', alternateRoots);
module.exports = {
getBlacklistRE: function() {
return blacklist(moduleBlacklist);
},
getProjectRoots() {
return [
// Keep your project directory.
path.resolve(__dirname)
].concat(alternateRoots);
},
extraNodeModules,
getPolyfills
};
@marioharper really, RN automatically checks for a rn-cli.config.js file in the project root. Why arenβt things like that documented anywhere and for what obscure reason doesnβt it check for metro.config.js, which is the documented way to call the config file. Iβll test this and potentially update the name of the config file in our script.
Really good question @MrLoh! For the transformers, polyfills, blacklists, and other stuff needed for my project (such as an universally incredibly legendarily complex and difficult problem to solve--that only the God's have the recipe to solve--such as resolving symlinks ;) ) in rn-cli.config.js I had to deep dive into their source code to figure it out. It would be absolutely fantastic if the teams responsible would better document this stuff.
@th317erd Thanks for the script. It needed some small tweaks to work with org-namespaced modules, but was a big help.
@cpjolicoeur No problem! If your changes can be contributed to help other people pass them off to me and I will update my post
for me @th317erd's solution did not work, still getting the duplicate module definition error when I try to start my react native app. @MrLoh's solution worked for me, if I rename the created metro.config.js to rn-cli.config.js. Unfortunatley, the commant react-native run-ios then only starts the packager, the app seems not to be started automatically anymore. Does anyone have a clue on how I could make this work again?
EDIT: It does work, my issue doesn't seem to be related to rn-cli.config.js
@cpjolicoeur what modifications did you make for org packages? @th317erd maybe we can put your script on a gist somewhere?
@ProLoser I wouldn't mind at all. I would like to hear what modifications @cpjolicoeur made before we do though.
@th317erd Just wanted to mention that I was able to get your script working with Node 6, but looks like it fails with Node 8. Not sure what the difference is between the two. Looks like it's failing to find react for the linked module in the hastemap, even though react is listed in the extraNodeModules properly.
Disregard that first bit, just figured out the problem was that I put the rn-cli config into a bin/ directory and it needed to be at the root.
I also had to make a small and ugly modification to get this working with scoped packages. I modified the getSymlinkedModules function like so:
function getSymlinkedModules() {
var alternateRoots = [];
for (let i = 0, il = baseModules.length; i < il; i++) {
let file = baseModules[i];
if (file.charAt(0) !== '.') {
if (file.charAt(0) === '@') {
const scopedModules = fs.readdirSync(path.join(baseModulePath, file))
scopedModules.forEach(innerFile => {
if (innerFile.charAt(0) !== '.') {
let fullInnerFile = path.join(baseModulePath, file, innerFile),
innerStats = fs.lstatSync(fullInnerFile);
if (innerStats.isSymbolicLink()) {
let realPath = fs.realpathSync(fullInnerFile);
if (realPath.substring(0, (__dirname).length) !== __dirname)
alternateRoots.push(realPath);
}
}
})
}
else {
let fullFile = path.join(baseModulePath, file),
stats = fs.lstatSync(fullFile);
if (stats.isSymbolicLink()) {
let realPath = fs.realpathSync(fullFile);
if (realPath.substring(0, (__dirname).length) !== __dirname)
alternateRoots.push(realPath);
}
}
}
}
return alternateRoots;
}
Quick and dirty but it works
@deusd Thank you for the reply for scoped modules! @deusd @ProLoser @cpjolicoeur I updated my code above with the refined work of @deusd (thank you!). If one of you could test for me and let me know if it works for you (I don't currently use scoped modules) that would be fantastic. If it works I will add it to a gist.
@deusd @ProLoser @cpjolicoeur I realized this morning that I was referencing getPolyfills in the above script but didn't define it (in this public posting of the script). I have now updated the script to include custom polyfills (if the specified file ./polyfills.js exists in your project).
Sorry, just catching up on this now. Been out for a while. I'll try to clean up my script to remove our hard-coded/personalized features and post what I used for orgs as well.
I updatet the metro-with-symlinks pacakge to create a rn-cli.config.js file that is used automatically now and updated the readme.
BTW scoped packages don't work in extraNodeModules currently, until facebook/metro#173 is merged
@th317erd just tried your updated script, it's not including the scoped modules for me in that version. Also has to remove the line that included ./transformer to get it to work not sure if that was required. Glad the snippet was useful!
Thanks @deusd ! Let me know if / how you fix it so I can update the script. Yeah... the transformer isn't supposed to be in there...
This is very quick and dirty, but it works. I'm very new to the javascript toolchain, but I've been slinging makefiles for a long time. Goes into web/src/Makefile and app/src/Makefile; assumes that web, lib, and app are siblings.
### Makefile to get around lack of symlink support
# Rule to make subdirectories
lib/%/: ../../lib/%/
mkdir -p $@
# Rule to copy source files from the shared library
lib/%.js: ../../lib/%.js
cp -a $< $@
DIRS = $(addsuffix /,$(shell cd ../..; find lib -type d -print))
LIBS = $(shell cd ../..; find lib -name '*.js' -print | grep -v '[\#~]')
all:: $(DIRS)
all:: $(LIBS) | $(DIRS) # require the subdirs before copying files
.PHONY: build
build: $(LIBS) | $(DIRS)
npm run build
You have to run make whenever you make a change in the library. If you keep forgetting, just run
while :; do sleep 10; make; done &
A simpler working rn-cli.config.js (but requires manual definition of linked modules):
const path = require('path');
const blacklist = require('metro/src/blacklist');
const LINKED_LIBS = [path.resolve(process.env.HOME, 'Developer/react-native/react-native-elements')];
module.exports = {
extraNodeModules: {
'react-native': path.resolve(__dirname, 'node_modules/react-native')
},
getProjectRoots() {
return [
path.resolve(__dirname),
...LINKED_LIBS
];
},
getBlacklistRE: function() {
return blacklist(LINKED_LIBS.map(lib => new RegExp(`${lib}/node_modules/react-native/.*`)));
}
};
The OP was never clear about what exactly is broken with symlinks, but I'm going to assume he's only talking about registering symlinks with the file watcher (since that's what everyone is talking about).
The following solution:
npm install get-dev-paths --only=dev
Add the following snippet to your metro.config.js module
const fs = require('fs')
const getDevPaths = require('get-dev-paths')
const projectRoot = __dirname
module.exports = {
// Old way
getProjectRoots: () => Array.from(new Set(
getDevPaths(projectRoot).map($ => fs.realpathSync($))
)),
// New way
watchFolders: Array.from(new Set(
getDevPaths(projectRoot).map($ => fs.realpathSync($))
))
}
metro adopt this technique to achieve a zero-config solution?i'm not really sure what is broken right now cause i'm using metro with yarn workspaces without any issue. I just have a simple custom config
@Titozzz Can you share your config? Would love to use metro again. We currently use haul and it's considerably slower.
Yarn workspaces are working fine because every dependency is hoisted to the root of the workspace. The build issue that arises with symlinks is when you need to link an external dependency (eg. in my case react-native-elements where I had to patch things and test against my app). So the workflow is:
npm install; npm linknpm link react-native-elementsThe issue arise because there is conflicting multiple react-native* dependencies (the one in your project & the one in the external lib, eg. react-native-elements).
However these build issues are to be expected and do happen with webpack as well (multiple copies of react errors, etc.)
It is fixable in a not really user-friendly way with getBlacklistRE.
With webpack, it is a bit more simple with the resolve option:
config.resolve.alias['react-native-elements'] = path.join(modulesPath, 'react-native-elements');
I think the two actionable things could be:
getBlacklistRE).@mgcrea Please open a new issue for that specifically, so people can more easily find the solution.
Anyone else who finds a problem with symlinks should also open a new issue, instead of posting it here. I'm in favor of locking this mega-thread so the individual problems with symlinks can be addressed separately.
@mgcrea Actually I also need the blacklist cause in one workspace a have multiple app so i'm using this to exclude any other react native than mine (I'm using no-hoist on react-native-* packages to allow multiple versions to coexist):
blacklistRE: /(nameOfRootDirectory|packages[/\\](?!nameOfCurrentPackage).*)[/\\]node_modules[/\\]react-native[/\\]/,
_(Replace nameOfRootDirectory and nameOfCurrentPackage)_
@jwaldrip Please DM me on twitter so we don't spam here if you need further help
@aleclarson I agree, this thread should be locked and closed, and any new issue should be treated separately with an repro example and a use case.
@aleclarson @Titozzz Do you mind explaining why you think this thread should be closed? As far as I can see, metro still does not support symlinks.
As @mgcrea state:
Yarn workspaces are working fine because every dependency is hoisted to the root of the workspace.
This thread is specifically about symlink support. Lerna / Yarn may individually work, but that does not mean that symlinks are working.
--
@aleclarson, I'm also not sure I understand the rationale here:
Anyone else who finds a problem with symlinks should also open a new issue, instead of posting it here.
Wouldn't it be harder to track the status of symlink support if it were spread amongst multiple issues?
--
Should metro adopt this technique to achieve a zero-config solution?
@aleclarson, if you think you solution should be built into metro, you could create PR? If metro maintainers accept, than perhaps this issue could be closed.... Until then, I personally think this thread should remain open. At the moment, It is the only method I have for being notified when/if progress is made.
Well I'm using yarn workspace with local packages that are symlinked to the root and it's working fine, so I'd like to find an example where it's not working. Why I think this thread could be closed is because it's a mix of lerna / yarn / symlinks with many different solutions to different problems, so I'm not sure if it's still relevant.
@jaridmargolin "Symlink support" is too vague, and this issue has too much stale information that newcomers have to read around.
There should be a new issue (probably created and maintained by a FB employee) where the OP states (a) which symlink-related features are missing, (b) which of those features are being worked on, and (c) what the blockers are for each missing feature. This issue would be updated when bugs are fixed or features are added.
Then, a new issue should be created for each encountered bug that's symlink-related. Each of these issues should be Github-labeled as "symlink-related" so people can easily find which bugs have been reported (via the issue search). This will keep the discussions focused on workarounds and progress updates for each specific bug.
Mega-threads are rarely the way to go. Not a good noise-to-signal ratio, IMO. You can Github-subscribe to the FB-maintained issue that reports on bug fixes and feature updates (related to symlinks), so you won't be missing out.
Of course, it's up to FB to do this, or leave the mega-thread as-is.
I've opened #257, which adds support for symlinks matching node_modules/* or node_modules/@*/*. This PR won't affect symlinks in arbitrary directories.
This means the module resolver will follow certain symlinks when necessary. PNPM works perfectly, for example!
PS: If you have symlinks in node_modules for packages you are developing, check out this comment to learn how to tell Metro to watch those packages automatically: https://github.com/facebook/metro/issues/1#issuecomment-421628147
Simple workaround using rsync (sorry Windows users): https://github.com/Swaagie/react-native-yunolink. This helped us to get unblocked for the time being. Basically provide 1..n targets to sync and let rsync do its magic. Also it will add targets to watchFolder.
Also it's nice to see some actual solutions finally getting solidified in code.
I also ended up using rsync scripts to make this work, but it was after I finished that I found wix/wml, which should work on windows and has a nice clean set of commands + saving links to a config file for a team to use.
I haven't tested it myself, but it looks like the easiest and most straightforward to accomplish this.
Began moving my repositories to a monorepo yesterday (MacOS), but got stuck on this. Is this still not fixed? :(
@lazaronixon Okay, try opening an issue here. Include your node, metro, and OS versions. Include your definition of "not working" in the issue. Thanks!
@lazaronixon Okay, try opening an issue here. Include your
node,metro, and OS versions. Include your definition of "not working" in the issue. Thanks!
Fixed. In the end it was missing @babel/runtime on library folder.
i combined answer from @aleclarson with lerna and hoisted react-native to root node_modules of monorepo.
const fs = require('fs')
const path = require('path')
const getDevPaths = require('get-dev-paths')
const blacklist = require('metro-config/src/defaults/blacklist')
const projectRoot = __dirname
const modules = getDevPaths(projectRoot).map($ => fs.realpathSync($))
const blacklisted = modules.map(module => new RegExp(`${module}/node_modules/react-native/.*`))
module.exports = {
watchFolders: Array.from(new Set(modules)),
resolver: {
extraNodeModules: {
'react-native': path.resolve(projectRoot, 'node_modules/react-native'),
'@babel/runtime': path.resolve(projectRoot, 'node_modules/@babel/runtime'),
"lodash": path.resolve(projectRoot, 'node_modules/lodash')
},
blacklistRE: blacklist(blacklisted),
providesModuleNodeModules: [path.resolve(projectRoot, '../../node_modules')]
}
}
And metro symlinks works for me.
This was working, no longer in 57.5 :(
This was working, no longer in 57.5 :(
I made this example https://github.com/fixerteam/React-Native-monorepo with 0.57.8 version.
Not working in 57.8 either. Not using lerna, just regular metro/cli and some local modules being worked on. I was able to use @Swaagie 's package to get around the issue for now. Not ideal ><
Here we go! Yet (another) update. This is tested and working with RN 0.57.8. The way it works is it uses metros new "extraNodeModules" to map external modules back to the root project. It builds a full module map (including modules from external roots), and "redirects" what it can back to the root project modules.
_Note: This also works with scoped modules_
!!! IT SURE WOULD BE NICE IF THE METRO TEAM WOULD NATIVELY SUPPORT THIS AND STOP IGNORING THE COMMUNITY !!!
<root>/rn-cli.config.js
function resolvePath(...parts) {
var thisPath = PATH.resolve.apply(PATH, parts);
if (!FS.existsSync(thisPath))
return;
return FS.realpathSync(thisPath);
}
function isExternalModule(modulePath) {
return (modulePath.substring(0, (__dirname).length) !== __dirname);
}
function listDirectories(rootPath, cb) {
FS.readdirSync(rootPath).forEach((fileName) => {
if (fileName.charAt(0) === '.')
return;
var fullFileName = PATH.join(rootPath, fileName),
stats = FS.lstatSync(fullFileName),
symbolic = false;
if (stats.isSymbolicLink()) {
fullFileName = resolvePath(fullFileName);
if (!fullFileName)
return;
stats = FS.lstatSync(fullFileName);
symbolic = true;
}
if (!stats.isDirectory())
return;
var external = isExternalModule(fullFileName);
cb({ rootPath, symbolic, external, fullFileName, fileName });
});
}
function buildFullModuleMap(moduleRoot, mainModuleMap, externalModuleMap, _alreadyVisited, _prefix) {
if (!moduleRoot)
return;
var alreadyVisited = _alreadyVisited || {},
prefix = _prefix;
if (alreadyVisited && alreadyVisited.hasOwnProperty(moduleRoot))
return;
alreadyVisited[moduleRoot] = true;
listDirectories(moduleRoot, ({ fileName, fullFileName, symbolic, external }) => {
if (symbolic)
return buildFullModuleMap(resolvePath(fullFileName, 'node_modules'), mainModuleMap, externalModuleMap, alreadyVisited);
var moduleMap = (external) ? externalModuleMap : mainModuleMap,
moduleName = (prefix) ? PATH.join(prefix, fileName) : fileName;
if (fileName.charAt(0) !== '@')
moduleMap[moduleName] = fullFileName;
else
return buildFullModuleMap(fullFileName, mainModuleMap, externalModuleMap, alreadyVisited, fileName);
});
}
function buildModuleResolutionMap() {
var moduleMap = {},
externalModuleMap = {};
buildFullModuleMap(baseModulePath, moduleMap, externalModuleMap);
// Root project modules take precedence over external modules
return Object.assign({}, externalModuleMap, moduleMap);
}
function findAlernateRoots(moduleRoot = baseModulePath, alternateRoots = [], _alreadyVisited) {
var alreadyVisited = _alreadyVisited || {};
if (alreadyVisited && alreadyVisited.hasOwnProperty(moduleRoot))
return;
alreadyVisited[moduleRoot] = true;
listDirectories(moduleRoot, ({ fullFileName, fileName, external }) => {
if (fileName.charAt(0) !== '@') {
if (external)
alternateRoots.push(fullFileName);
} else {
findAlernateRoots(fullFileName, alternateRoots, alreadyVisited);
}
});
return alternateRoots;
}
function getPolyfillHelper() {
var getPolyfills;
// Get default react-native polyfills
try {
getPolyfills = require('react-native/rn-get-polyfills');
} catch(e) {
getPolyfills = () => [];
}
// See if project has custom polyfills, if so, include the PATH to them
try {
let customPolyfills = require.resolve('./polyfills.js');
getPolyfills = (function(originalGetPolyfills) {
return () => originalGetPolyfills().concat(customPolyfills);
})(getPolyfills);
} catch(e) {}
return getPolyfills;
}
const PATH = require('path');
const FS = require('fs'),
blacklist = require('metro-config/src/defaults/blacklist');
const moduleBlacklist = [
/public.*/,
/node_modules[^\/]+\/.*/,
/[^-]build\..*/,
/.*\.bak\/.*/,
/app\/pages\/browser\/.*/
],
baseModulePath = resolvePath(__dirname, 'node_modules'),
// watch alternate roots (outside of project root)
alternateRoots = findAlernateRoots(),
// build full module map for proper
// resolution of modules in external roots
extraNodeModules = buildModuleResolutionMap();
if (alternateRoots && alternateRoots.length)
console.log('Found alternate project roots: ', alternateRoots);
module.exports = {
resolver: {
blacklistRE: blacklist(moduleBlacklist),
extraNodeModules,
useWatchman: false
},
watchFolders: [PATH.resolve(__dirname)].concat(alternateRoots),
transformer: {
babelTransformerPath: require.resolve('./compiler/transformer')
},
serializer: {
getPolyfills: getPolyfillHelper()
}
};
Even with @th317erd script, I would get duplicate "react-native" modules errors by the packager.
I modified the script to create a blacklist to exclude "react-native" from the linked modules:
const PATH = require('path');
const FS = require('fs'),
blacklist = require('metro-config/src/defaults/blacklist');
function resolvePath(...parts) {
var thisPath = PATH.resolve.apply(PATH, parts);
if (!FS.existsSync(thisPath))
return;
return FS.realpathSync(thisPath);
}
function isExternalModule(modulePath) {
return (modulePath.substring(0, (__dirname).length) !== __dirname);
}
function listDirectories(rootPath, cb) {
FS.readdirSync(rootPath).forEach((fileName) => {
if (fileName.charAt(0) === '.')
return;
var fullFileName = PATH.join(rootPath, fileName),
stats = FS.lstatSync(fullFileName),
symbolic = false;
if (stats.isSymbolicLink()) {
fullFileName = resolvePath(fullFileName);
if (!fullFileName)
return;
stats = FS.lstatSync(fullFileName);
symbolic = true;
}
if (!stats.isDirectory())
return;
var external = isExternalModule(fullFileName);
cb({ rootPath, symbolic, external, fullFileName, fileName });
});
}
function buildFullModuleMap(moduleRoot, mainModuleMap, externalModuleMap, _alreadyVisited, _prefix) {
if (!moduleRoot)
return;
var alreadyVisited = _alreadyVisited || {},
prefix = _prefix;
if (alreadyVisited && alreadyVisited.hasOwnProperty(moduleRoot))
return;
listDirectories(moduleRoot, ({ fileName, fullFileName, symbolic, external }) => {
if (symbolic)
return buildFullModuleMap(resolvePath(fullFileName, 'node_modules'), mainModuleMap, externalModuleMap, alreadyVisited);
var moduleMap = (external) ? externalModuleMap : mainModuleMap,
moduleName = (prefix) ? PATH.join(prefix, fileName) : fileName;
if (fileName.charAt(0) !== '@')
moduleMap[moduleName] = fullFileName;
else
return buildFullModuleMap(fullFileName, mainModuleMap, externalModuleMap, alreadyVisited, fileName);
});
}
function buildModuleResolutionMap() {
var moduleMap = {},
externalModuleMap = {};
buildFullModuleMap(baseModulePath, moduleMap, externalModuleMap);
// Root project modules take precedence over external modules
return Object.assign({}, externalModuleMap, moduleMap);
}
function findAlernateRoots() {
var alternateRoots = [];
listDirectories(baseModulePath, ({ fullFileName, external }) => {
if (external)
alternateRoots.push(fullFileName);
});
return alternateRoots;
}
function getPolyfillHelper() {
var getPolyfills;
// Get default react-native polyfills
try {
getPolyfills = require('react-native/rn-get-polyfills');
} catch(e) {
getPolyfills = () => [];
}
// See if project has custom polyfills, if so, include the PATH to them
try {
let customPolyfills = require.resolve('./polyfills.js');
getPolyfills = (function(originalGetPolyfills) {
return () => originalGetPolyfills().concat(customPolyfills);
})(getPolyfills);
} catch(e) {}
return getPolyfills;
}
const baseModulePath = resolvePath(__dirname, 'node_modules'),
// watch alternate roots (outside of project root)
alternateRoots = findAlernateRoots(),
// build full module map for proper
// resolution of modules in external roots
extraNodeModules = buildModuleResolutionMap();
const moduleBlacklist = alternateRoots.map(
root => new RegExp(
PATH.join(root, 'node_modules', 'react-native')
.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g, '\\$&')
)
);
if (alternateRoots && alternateRoots.length)
console.log('Found alternate project roots: ', alternateRoots);
module.exports = {
resolver: {
blacklistRE: blacklist(moduleBlacklist),
extraNodeModules
},
watchFolders: alternateRoots,
serializer: {
getPolyfills: getPolyfillHelper()
}
};
Also, it seems that configurations are merged so that watchFolders: alternateRoots, is enough and the root folder is included by default.
@joerneu Thanks! Good finds. It would make sense I guess that react-native and the current root path are defaulted.
@joerneu I got the jest-haste-map duplicate modules problem even with your script.
I am using RN 0.55.2 (I changed blacklist to const blacklist = require('metro/src/blacklist'); )
Do you think it matters that I have a nested packages directory?
For example my structure is like so:
````
root
- node_modules
*react-native
- packages
- mobile (native)
- node_modules
*react-native
````
I have tried running the start cli script from the root directory and the mobile directory, with the config file in the root of each when I try. If I do it from the mobile directory I don't get any warnings but cant access the root node modules so i guess it's supposed to go from the root directory and make its way inwards?
The thing is for either if I console log this part:
````
listDirectories(baseModulePath, ({ fullFileName, external }) => {
console.log('external: ', external);
if (external)
alternateRoots.push(fullFileName);
});
`````
I'm not getting any 'external' paths. Is this maybe because of the extra layer of 'packages' directory?
I feel like it should be getting some external paths? The alternate roots array always ends up empty.
Is there some part of the script I can adjust do you think to accomodate the extra folder layer?
If either you or @th317erd could suggest how I might fix this it would be life saving, have been banging my head against wall on this for 3 days now, and this config stuff is beyond me...
The only way I can get it to work is to delete the react-native folder from the mobile package node modules after a yarn install because yarn insists on installing it there even though I've moved to only declaring it as a dependency in the root package.json.... gah
EDIT ok that doesn't work because I need react native package in the mobile node_modules to run react-native run-android etc....
Thanks
@SamMatthewsIsACommonName I am not certain, but I think RN 0.55 (and the related metro modules) are too old for this updated code. This is an older version I have that essentially does the same thing. Remove what you don't need, and modify what you want. Hopefully this will make a difference for you:
const path = require('path');
const fs = require('fs');
let blacklist,
getPolyfills;
// Get blacklist factory
try {
blacklist = require('metro-config/src/defaults/blacklist');
} catch (e) {
try {
blacklist = require('metro-bundler/src/blacklist');
} catch(e) {
blacklist = require('metro/src/blacklist');
}
}
// Get default react-native polyfills
try {
getPolyfills = require('react-native/rn-get-polyfills');
} catch(e) {
getPolyfills = () => [];
}
// See if project has custom polyfills, if so, include the path to them
try {
let customPolyfills = require.resolve('./polyfills.js');
getPolyfills = (function(originalGetPolyfills) {
return () => originalGetPolyfills().concat(customPolyfills);
})(getPolyfills);
} catch(e) {}
const moduleBlacklist = [
/public.*/,
/node_modules[^\/]+\/.*/,
/[^-]build\..*/,
/.*\.bak\/.*/
];
const baseModulePath = path.resolve(__dirname, 'node_modules');
function getSymlinkedModules() {
function checkModule(fileName, alternateRoots, modulePath) {
try {
let fullFileName = path.join(modulePath, fileName),
stats = fs.lstatSync(fullFileName);
if (stats.isSymbolicLink()) {
let realPath = fs.realpathSync(fullFileName);
if (realPath.substring(0, (__dirname).length) !== __dirname)
alternateRoots.push(realPath);
}
} catch (e) {}
}
function checkAllModules(modulePath, alternateRoots) {
var stats = fs.lstatSync(modulePath);
if (!stats.isDirectory())
return alternateRoots;
fs.readdirSync(modulePath).forEach((fileName) => {
if (fileName.charAt(0) === '.')
return;
if (fileName.charAt(0) !== '@')
checkModule(fileName, alternateRoots, modulePath);
else
checkAllModules(path.join(modulePath, fileName), alternateRoots);
});
return alternateRoots;
}
return checkAllModules(baseModulePath, []);
}
function getExtraModulesForAlternateRoot(fullPath) {
var packagePath = path.join(fullPath, 'package.json'),
packageJSON = require(packagePath),
alternateModules = [].concat(Object.keys(packageJSON.dependencies || {}), Object.keys(packageJSON.devDependencies || {}), Object.keys(packageJSON.peerDependencies || {})),
extraModules = {};
for (var i = 0, il = alternateModules.length; i < il; i++) {
var moduleName = alternateModules[i];
extraModules[moduleName] = path.join(baseModulePath, moduleName);
}
return extraModules;
}
//alternate roots (outside of project root)
var alternateRoots = getSymlinkedModules(),
//resolve external package dependencies by forcing them to look into path.join(__dirname, "node_modules")
extraNodeModules = alternateRoots.reduce((obj, item) => {
Object.assign(obj, getExtraModulesForAlternateRoot(item));
return obj;
}, {});
if (alternateRoots && alternateRoots.length)
console.log('Found alternate project roots: ', alternateRoots);
module.exports = {
getBlacklistRE: function() {
return blacklist(moduleBlacklist);
},
getTransformModulePath: function() {
return require.resolve('./compiler/transformer');
},
getProjectRoots() {
return [
// Keep your project directory.
path.resolve(__dirname)
].concat(alternateRoots);
},
extraNodeModules,
getPolyfills
};
@th317erd Great thanks so much! I'll give it a try and see how I get on. Just to confirm, should this be run from the root directory or the mobile (react native) directory?
@SamMatthewsIsACommonName I am not sure exactly how your scripts are working, but I believe it should be in your root directory, in a file called rn-cli.config.js
Ok great thanks. I guess I mean whether you call the react native cli script from the version of react native at the root or deeper in the project. I think the issue is I'm trying this with yarn workspace and it doesn't seem to find symlinks. I'm going to have a try moving to just lerna and see how I get on. God this is confusing
Yeah, sorry @SamMatthewsIsACommonName , the react tool chain is confusing a frustrating at best, down right terrible and shameful at worst. I wish I had happy news for you. :cry:
@SamMatthewsIsACommonName , yarn workspaces work but you have to use "nohoist":
{
"private": true,
"workspaces": {
"packages": ["app-native", "component-native"],
"nohoist": ["**/*"]
}
}
"nohoist" leaves everything in each package's node_modules folder, but linking still works. (Tested with latest yarn and React Native. Theoretically, it would be enough to "nohoist" React Native but I also got issues with Jest and it was not worth the trouble.)
rn-cli.config.js should be in your React Native app project folder - not in the yarn workspace root. (With the example above, it should be in the "app-native" folder.)
@joerneu thanks so much, that did it. I didn't realise you could just nohoist all like that. What is crazy is I had no hoisted react native plus a lot of other RN related packages explicitly, and it would respect all except RN which it would hoist every time. Then just to make things even more interesting once I got the hang of the config file and blacklisting it, it would bundle once ignoring the other RN then the next time would pick it up as a duplicate again! Anyway thanks a lot
ModuleResolver
where to import ModuleResolver from ??
Hi community !
Still no real solution to use npm modules locally ?
To anyone who is using my script :point_up:, I just updated it to properly work with symlinked scoped modules. I don't use yarn, so please follow the advice of @joerneu if you are using yarn.
For anyone it might help; I came up with the following workaround when upgrading a project to RN 0.57.8 this week (from RN 0.55.4). I have the RN app itself and two linked dependencies, one which is depended on by the other (App -> linked-dep-1 -> linked-dep-2).
Main RN project package.json's dependencies:
"dependencies": {
"linked-dep-1": "link:../linked-dep-1",
...
}
linked-dep-1's package.json dependencies:
"dependencies": {
"linked-dep-2": "link:../linked-dep-2",
...
}
Main project's rn-cli.config.js:
````javascript
const metro = require('metro')
const path = require('path')
module.exports = {
resolver: {
blacklistRE: metro.createBlacklist([
/linked-dep-1\/node_modules\/./,
/linked-dep-2\/node_modules\/./
]),
extraNodeModules: new Proxy({}, {
get: (target, name) => path.join(process.cwd(), node_modules/${name})
})
},
watchFolders: [
path.join(process.cwd(), '../linked-dep-1'),
path.join(process.cwd(), '../linked-dep-2'),
],
}
````
I could have potentially made the blacklistRE + watchFolders dynamically build but since I only have a couple of linked dependencies it wasn't worth me doing.
Here my version of rn-cli.config.js with the latest changes by @th317erd. (See Comment)
const PATH = require('path');
const FS = require('fs'),
blacklist = require('metro-config/src/defaults/blacklist');
function resolvePath(...parts) {
var thisPath = PATH.resolve.apply(PATH, parts);
if (!FS.existsSync(thisPath))
return;
return FS.realpathSync(thisPath);
}
function isExternalModule(modulePath) {
return (modulePath.substring(0, (__dirname).length) !== __dirname);
}
function listDirectories(rootPath, cb) {
FS.readdirSync(rootPath).forEach((fileName) => {
if (fileName.charAt(0) === '.')
return;
var fullFileName = PATH.join(rootPath, fileName),
stats = FS.lstatSync(fullFileName),
symbolic = false;
if (stats.isSymbolicLink()) {
fullFileName = resolvePath(fullFileName);
if (!fullFileName)
return;
stats = FS.lstatSync(fullFileName);
symbolic = true;
}
if (!stats.isDirectory())
return;
var external = isExternalModule(fullFileName);
cb({ rootPath, symbolic, external, fullFileName, fileName });
});
}
function buildFullModuleMap(moduleRoot, mainModuleMap, externalModuleMap, _alreadyVisited, _prefix) {
if (!moduleRoot)
return;
var alreadyVisited = _alreadyVisited || {},
prefix = _prefix;
if (alreadyVisited && alreadyVisited.hasOwnProperty(moduleRoot))
return;
listDirectories(moduleRoot, ({ fileName, fullFileName, symbolic, external }) => {
if (symbolic)
return buildFullModuleMap(resolvePath(fullFileName, 'node_modules'), mainModuleMap, externalModuleMap, alreadyVisited);
var moduleMap = (external) ? externalModuleMap : mainModuleMap,
moduleName = (prefix) ? PATH.join(prefix, fileName) : fileName;
if (fileName.charAt(0) !== '@')
moduleMap[moduleName] = fullFileName;
else
return buildFullModuleMap(fullFileName, mainModuleMap, externalModuleMap, alreadyVisited, fileName);
});
}
function buildModuleResolutionMap() {
var moduleMap = {},
externalModuleMap = {};
buildFullModuleMap(baseModulePath, moduleMap, externalModuleMap);
// Root project modules take precedence over external modules
return Object.assign({}, externalModuleMap, moduleMap);
}
function findAlernateRoots(moduleRoot = baseModulePath, alternateRoots = [], _alreadyVisited) {
var alreadyVisited = _alreadyVisited || {};
if (alreadyVisited && alreadyVisited.hasOwnProperty(moduleRoot))
return;
alreadyVisited[moduleRoot] = true;
listDirectories(moduleRoot, ({ fullFileName, fileName, external }) => {
if (fileName.charAt(0) !== '@') {
if (external)
alternateRoots.push(fullFileName);
} else {
findAlernateRoots(fullFileName, alternateRoots, alreadyVisited);
}
});
return alternateRoots;
}
function getPolyfillHelper() {
var getPolyfills;
// Get default react-native polyfills
try {
getPolyfills = require('react-native/rn-get-polyfills');
} catch(e) {
getPolyfills = () => [];
}
// See if project has custom polyfills, if so, include the PATH to them
try {
let customPolyfills = require.resolve('./polyfills.js');
getPolyfills = (function(originalGetPolyfills) {
return () => originalGetPolyfills().concat(customPolyfills);
})(getPolyfills);
} catch(e) {}
return getPolyfills;
}
const baseModulePath = resolvePath(__dirname, 'node_modules'),
// watch alternate roots (outside of project root)
alternateRoots = findAlernateRoots(),
// build full module map for proper
// resolution of modules in external roots
extraNodeModules = buildModuleResolutionMap();
const moduleBlacklist = alternateRoots.map(
root => new RegExp(
PATH.join(root, 'node_modules', 'react-native')
.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g, '\\$&')
)
);
if (alternateRoots && alternateRoots.length)
console.log('Found alternate project roots: ', alternateRoots);
module.exports = {
resolver: {
blacklistRE: blacklist(moduleBlacklist),
extraNodeModules
},
watchFolders: alternateRoots,
serializer: {
getPolyfills: getPolyfillHelper()
}
};
@liamjones that is brilliant! I didn't think of using proxies to resolve real-time. Awesome!
Hey hi @th317erd I'm using your config successfully with normal linked module, unfortunately the scoped linked module does not works for me with latest rn version. I saw the note in the code so here is my feedback ;)
Basically I just had to pass down modulePath to checkModule function and use modulePath instead of baseModulePath. modulePath being baseModulePath with the scoped folder name at the end.
const baseModulePath = path.resolve(__dirname, 'node_modules');
// NOTE: Scoped modules hasn't been fully tested yet. Please respond to
// let th317erd know if this code works with scoped modules
function getSymlinkedModules() {
function checkModule(fileName, alternateRoots, modulePath) {
try {
let fullFileName = path.join(modulePath, fileName),
stats = fs.lstatSync(fullFileName);
if (stats.isSymbolicLink()) {
let realPath = fs.realpathSync(fullFileName);
if (realPath.substring(0, (__dirname).length) !== __dirname)
alternateRoots.push(realPath);
}
} catch (e) {}
}
function checkAllModules(modulePath, alternateRoots) {
var stats = fs.lstatSync(modulePath);
if (!stats.isDirectory())
return alternateRoots;
fs.readdirSync(modulePath).forEach((fileName) => {
if (fileName.charAt(0) === '.')
return;
if (fileName.charAt(0) !== '@')
checkModule(fileName, alternateRoots, modulePath);
else
checkAllModules(path.join(modulePath, fileName), alternateRoots);
});
return alternateRoots;
}
return checkAllModules(baseModulePath, []);
}
With latest rn version it's also needed to replace require('metro-bundler/src/blacklist') or require('metro/src/blacklist') by the valid path require('metro-config/src/defaults/blacklist')
Thank you so much @mbret! I updated my post with the script to apply your findings. :smiley:
Is this on the roadmap to be actually _fixed_ at some point?
This is a completely ridiculous problem to have. It's been two years - get some intern to fix the problem with the bundler already.
@denchp I will be honest with you, I don't understand why we are still waiting for such a basic feature. I mean whoever want to develop a library kind of needs it. Building a library for a framework is such a common task. Fun part is that I have been using theses custom metro config since the beginning of my usage of react native, a couple of years ago. I bet a massive amount of people went on this issue as well.
But I'm pretty sure they are aware and already started working on it.
Anyone who wants Metro to have built-in symlink support, please show your support on https://github.com/facebook/jest/pull/7549, which must be merged before https://github.com/facebook/metro/pull/257 can be. Thanks!
Please refrain from posting your long config files here, so others can see this comment.
I'm working on a project that has two different react-native apps that work from the same backend.
I'm wanting to keep the theme with native-base consistent across both apps.
I've tried symlinking one app's native-base-theme folder, but that is causing import errors which lead me to this thread while digging through other issue threads.
Has anyone tried anything similar?
The native-base-theme folder is not located in node_modules, so I was wondering if there may be some kind of different way to go about it.
The native-base-theme folder is in the root where .expo and App.js are.
Symlink support would be awesome :/
@mccordgh Show your support here: https://github.com/facebook/metro/issues/1#issuecomment-457955034
While you wait, you may want to try one of these: https://github.com/facebook/metro/issues/1#issuecomment-421628147 or https://github.com/facebook/metro/issues/1#issuecomment-448064559
@aleclarson Thanks!
I'm not sure exactly how I use rn-cli.config.js to point to the external native-base-theme folder?
I run rn-cli.config.js from root of my project?
Where do I point to the external native-base-theme folder that I want?
@mccordgh rn-cli.config.js is automatically picked up by the packager if it exists in the root of your project. If you look at the scripts above, essentially what is happening is a full (or partial... which would probably be easier in your case) module resolution map is being built, which includes external folders. It really isn't much more complicated then this: You are manually resolving a module outside the project root. There are two main things happening in all the above scripts: extraNodeModules is being used to tell the packager where things are (I believe it must be a complete list, hence why you will see the use of a Proxy, and in other scripts a complete module scan/list). The other is watchFolders, which simply trigger the packager to recompile if things change in the watched folder. You will want both. Inject your "external" module into the extraNodeModules module map, and add your "external" module to watchFolders
You should be able to use my script without any problems (pay attention, as there are two scripts above from me, one is for older RN versions, and one is for newer versions). Simply place the contents of the above script into rn-cli.config.js as a file at the root of your project (sibling of package.json). Then add a file dependency to your package.json ("native-base-theme": "file://../native-base-theme/"), npm i (watch out! I have had issues with NPM complaining in this situation when there is also a node_modules inside the "external" module), and restart the packager. You should be good to go at this point.
@th317erd Thanks!
I'm not sure how to "inject" my external module in to the extraNodeModules and watchFolders.
I see extraNodeModules is being set to buildModuleResolutionMap() which returns a new object after calling buildFullModuleMap(...), but I don't see an obvious place to add the path to my external module.
So far I have copied over the code for rn-cli.config.js from @joerneu's comment on Jan 11.
Then I added to my project's package.json
"dependencies": {
"native-base-theme": "file://../themes/native-base-theme/",
...
}
When I run npm i I get:
npm ERR! Could not install from "../themes/native-base-theme" as it does not contain a package.json file.
one level above my project's root is themes/native-base-theme where native-base-theme/ is the ejected NativeBase theme folder moved up one level.
I'm not sure if this may be a slightly different case as I'm not trying to have like a global node_modules or anything, just have a one global theme folder for all the apps just like the one that is created when in a project's root when you would run the command to eject the native-base theme.
@mccordgh Yes, native-base-theme obviously must be a node module with a package.json before this will work. Just add a package.json (with the correct information) to native-base-theme and it should work.
As for the extraNodeModules, if you are using one of the above scripts that uses buildFullModuleMap then you don't need to do anything. After npm i a symlink will be created to your external module (I am assuming you aren't on a Windows machine... which maybe is a bad assumption), and then you shouldn't need to do anything except restart the packager.
If you are on a machine without symlink support (i.e. Windows), you might need to add it manually with a resolved absolute path: i.e. extraNodeModules: Object.assign({}, extraNodeModules, { "native-base-theme": path.resolve(__dirname, '..', 'native-base-theme') }); This would also mean that you would no longer want it to be a local model listed in your main projects package.json
@th317erd I was able to get it work! Thanks a ton!
The others on my team can even just run npm install and it all works, which is awesome.
the watcher is also working like a charm!
When I change anything in the theme folder, the expo app will reload on my phone π very awesome.
My structure looks like this (_only showing important files_):
my-app/ and themes/ are in the same directory.
/User/me/workspace/my-project/
|
+--my-app/
|
+-- App.js
+-- package.json
+-- rn-cli-config.js
+-- (etc project files)
|
+--themes/
|
+-- native-base-theme
|
+-- components
|
+-- index.js (compiles everything to one export)
+-- package.json (created with 'npm init')
|
+-- variables
|
+-- platform.js (exports theme variables I am using/modifying)
+-- package.json (created with 'npm init')
themes/native-base-theme/components/package.json:
_set entry point to index.js by setting "main" key._
{
"name": "custom-theme-components",
"version": "1.0.0",
"description": "custom-theme-components",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
themes/native-base-theme/variables/package.json:
_set entry point to platform.js by setting "main" key._
{
"name": "custom-theme-variables",
"version": "1.0.0",
"description": "custom-theme-variables",
"main": "platform.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
In my-app's package.json:
"dependencies": {
...
"custom-theme-components": "file:../themes/native-base-theme/components",
"custom-theme-variables": "file:../themes/native-base-theme/variables",
},
App.js:
import getTheme from 'custom-theme-components';
import variables from 'custom-theme-variables'
...
render() {
return (
<Root>
<StyleProvider style={getTheme(variables)}>
<AppContainer />
</StyleProvider>
</Root>
);
}
...
rn-cli.config.js pasted directly from @joerneu's comment on Jan 11:
https://github.com/facebook/metro/issues/1#issuecomment-453649261
I then had to do the four steps for the Haste modules common error before it would properly compile:
$ watchman watch-del-all
$ rm -rf node_modules && npm install
$ rm -rf /tmp/metro-bundler-cache-*
$ rm -rf /temp/haste-map-react-native-packager-*
after that $ expo start and it worked <3
I will probably go back and clean it up a bit. might just be able to combine those in to one nice module and remove one of the steps down in to the directory.
ThereΒ΄s no news?
@rcerrejon https://github.com/facebook/metro/pull/257#pullrequestreview-208918934
Just a note that in 0.59 the config file rn-cli.config.js is changed to metro.config.js
(in reference to https://github.com/facebook/metro/issues/1#issuecomment-453649261)
It requires that you run [email protected] and [email protected].
I followed @liamjones config and add this to metro.config.js
extraNodeModules: new Proxy({}, {
get: (target, name) => path.join(process.cwd(), `node_modules/${name}`)
})
This solved for me.
[EDIT]
I have a final version for this: https://gist.github.com/jgcmarins/7ec11bc0af25f908afad9c1b38c3e426
Follwed this article. It works great now.
https://medium.com/@slavik_210/symlinks-on-react-native-ae73ed63e4a7
Add metro.config.js (require version of react-native >= 0.59) to your project root as below will solve this problem:
const path = require('path');
module.exports = {
watchFolders: [
path.resolve(__dirname, `${relativePathOfModule1}`),
path.resolve(__dirname, `${relativePathOfModule2}`),
...,
],
};
Hey everyone! I'm hoping to fix this issue very soon. It is most likely that symlink support will require opt-in via Metro's config, like the commenters above have already shared. I want to make sure we provide good documentation and a workable solution for this so that this is clearly communicated and people can easily opt-in to this behavior.
To make sure I get everything right, could you share some open source repositories (either real projects or some example projects you make) that you'd like to work with symlinks so that I can play around with them?
Here's an example repo for Metro + PNPM (which uses symlinks in node_modules):
@cpojer
Here is something I have been working on.
https://github.com/spoman007/reactXstarter
I have a core npm package that I am linking with mobile
@spoman007 I just built your project and I get the following error:
Loading dependency graph, done.
error: bundling failed: Error: Unable to resolve module `@babel/runtime/helpers/interopRequireDefault`
from `/Users/xxx/reactXstarter/core/index.js`: Module
`@babel/runtime/helpers/interopRequireDefault` does not exist in the Haste module map
I have the exact same config on my project (although I am using yarn workspaces) and I get the same error too. I cannot manage to know if it is because of metro or react-native.
After upgrade to 0.59 the config changed a bit.
Here is my working config for symlink, scoped package and hast module name collision avoidance.
The biggest change is the fix to avoid package name hast collision. Basically with this config you can install react native / react, etc inside your local external linked package and still import it to your main project. As long as it's listed as a peer dependency the package of your local module will be ignored in the main project.
https://gist.github.com/mbret/edbfec5df3adac6dbf980b43be08f556
@benoitvallon you can do a fresh clone of repository.
I have fixed the issue.
Hi @mbret, where should we keep this config file? I tried putting the contents of your sample file into the metro.config.js found inside the android project folder in the RN project which is supposed to use the local node module (outside of this RN project); But it resulted some build errors. I can imagine something is missing but not sure how/where?
Been using https://github.com/Swaagie/react-native-yunolink which the creator posted in another metro symlink issue.
Not ideal but gets the job done and it's watch files capability is nice for quick reloading node_modules when working on the JS portions of a RN module.
@cpojer Any update on this? Struggling with converting an existing RN project into a monorepo due to the issue. Tried yarn workspaces, lerna, metro config, ... there's always something failing :( :)
@bitcrumb I recently switched over to a monorepo (with yarn) and we got past the symlink problem by explicitly putting common dev dependencies (like the RN CLI) in the root package.json, and launching RN from the root with --projectRoot $CWD/packages/app --watchFolders $CWD.
This means that both the root of the monorepo and the specific package's node_modules will be used for looking up module resolutions. If packages/app uses packages/lib then they will both duplicate modules that will be hoisted to the root.
Since yarn _should_ be good at removing duplicates while hoisting, as long as you pass every symlinked project to --watchFolders things should just work.
I imagine this is less effective for non-monorepo symlinks because RN will find 2 identical modules in project folders.
I followed @jgcmarins solution above and modified it for my usage.
I followed @liamjones config and add this to
metro.config.jsextraNodeModules: new Proxy({}, { get: (target, name) => path.join(process.cwd(), `node_modules/${name}`) })This solved for me.
I had a slightly different use case. I'm building reusable components for my company's react-native apps and I wanted to have a test RN app embedded in the module so I could test without having to publish.
I modified the above solution to work for me, and have included my complete metro.config.js:
let path = require('path');
module.exports = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: false
}
})
},
resolver: {
/* This configuration allows you to build React-Native modules and
* test them without having to publish the module. Any exports provided
* by your source should be added to the "target" parameter. Any import
* not matched by a key in target will have to be located in the embedded
* app's node_modules directory.
*/
extraNodeModules: new Proxy(
/* The first argument to the Proxy constructor is passed as
* "target" to the "get" method below.
* Put the names of the libraries included in your reusable
* module as they would be imported when the module is actually used.
*/
{
'@namespace/reusableModule': path.resolve(__dirname, '../src/')
},
{
get: (target, name) =>
{
if (target.hasOwnProperty(name))
{
return target[name];
}
return path.join(process.cwd(), `node_modules/${name}`);
}
}
)
},
projectRoot: path.resolve(__dirname),
watchFolders: [
path.resolve(__dirname, '../src')
]
};
@jdpace2 your solution worked perfectly for me while working on a module which had the same structure:
- git
|- react-native-custom-module
|- CustomModuleExample
The only thing that I noticed was that I needed to modify the react-native-custom-module code while in the CustomModuleExample project (through the library, which isn't optimal but it works). If I attempted to code directly in the react-native-custom-module project it was using react-native 0.20.0 (instead of 0.59.9 which the example was using). There are a number of posts as to why this is happening, due to the peerDependencies of the react-native-library project.
At this point I can run without issues CustomModuleExample on a device/emulator.
But now if I attempt to use this module within another project, so I have something like this:
- git
|- react-native-custom-module
|- CustomModuleExample
|- AwesomeApp
I cannot use your fix in AwesomeApp because metro will now get a name conflict between AwesomeApp/node_modules/react-native and react-native-custom-module/CustomModuleExample/node_modules/react-native - to get around this I'm currently using:
npm install -g install-local
npm install
rm -rf node_modules/react-native-custom-module
install-local ../react-native-custom-module
and now both CustomModuleExample and AwesomeApp` are now running out of Android Studio. With regards to the module, not sure if it will ever be on npm (although that would resolve the dependency issue) but for now it's running well.
EDIT - I was able to get IOS built and running in much the same way. I have react-native-custom-module included in the libraries and I needed to follow a couple things that I found on SO:
After doing so, I'm able to do the same thing, where I'm editing react-native-custom-module/ios within the react-native-custom-module/CustomModulExample/ios project.
So, in terms of getting it working I'm in a decent enough spot where I can:
I guess my question is (probably way to vague) but I'm yet to find a good example of setting up a module editing development environment where the final structure will be what it is listed above.
Appreciate any info you can provide ahead of time! I'm definitely going to move this over to SO (and maybe remove this entry, but for now I want to leave it here as it documents what I've needed to do to get around the symlinks thing.
Here's my metro.config.js and package.json files with my explanation as to how to fix this problem :)
metro.config.js
const path = require('path');
module.exports = {
resolver: {
extraNodeModules: {
/*
* In an ideal world, the Metro bundler would support symlinks (my-lib is symlinked), but here we are.
* https://github.com/facebook/metro/issues/1
*
* So, here's the current workaround:
* - TS is setup as normal, and VS Code reads tsconfig.json, so from within Code, everything looks normal
* - We copy my-lib from ../../../some/far/away/path/my-lib to ./temp/my-lib (and watch for changes)
* - Babel is configured to look for my-lib content at ./temp/my-lib
* - Babel 7 compiles TS by dropping types, so we're free to force-feed it with ./temp/my-lib (as above)
* - Typechecking works as normal, since tsconfig.json has no idea about our switcharoo.
*
* tl;dr
* - TS (and VS Code) thinks my-lib is located at node_modules/my-lib via symlink
* - Babel thinks my-lib is located at ./temp/my-lib (because Metro doesn't do symlinks wtf)
* - We actively copy the real my-lib to ./temp/my-lib
*/
'my-lib': path.resolve(__dirname, 'temp/my-lib')
}
}
};
package.json
{
"scripts": {
"copy-my-lib": "cpx \"../../libs/my-lib/**/*\" \"./temp/my-lib\" --clean",
"copy-my-lib:watch": "cpx \"../../libs/my-lib/**/*\" \"./temp/my-lib\" --watch",
"start": "yarn copy-my-lib && concurrently --kill-others --names copy-my-lib,RN,tsc \"yarn copy-my-lib:watch\" \"react-native start\" \"tsc --watch\"",
"typecheck": "tsc --watch",
"watch": "concurrently -k \"yarn typecheck\" \"yarn start\""
}
}
@kenjdavidson Thank you for your reply, and I'm glad it worked for you!
Make sure your module's package.json file is set to include only the files you want to be published. See the "files" property in the JSON structure below. "readme.md" and "package.json" are always included and don't need to be explicitly listed.
{
"name": "@myNameSpace/react-native-custom-module",
"version": "0.4.2",
"description": "So custom.",
"main": "src/index.js",
"files": [
"/changes.md",
"/src"
],
"author": {
"name": "Kenneth Davidson"
},
"contributors": [
{"name": "J.D. Pace"}
],
"peerDependencies": {
"react": ">=16.8.5",
"react-native": ">=0.56.1"
},
"publishConfig": {
"registry": "https://my-local-npm.my-domain.com"
}
}
Two more things that helped me: use namespaces to organize your modules, and use Verdaccio to maintain a local NPM repository. You can associate specific namespaces with specific repositories! npm can pull @myNameSpace packages from a local private repo, and anything else from the global repo. I was trying to use Verdaccio as a caching module proxy, but didn't have consistent success.
In case someone is looking for a generic lerna work-around, without having to manually add packages to metro.config.js when your lerna.json changes, then
here's what worked for me
Needs the glob package
npm i -D glob
metro.config.js
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const root = fs.realpathSync(path.join(__dirname, '..'));
const lerna = require(path.join(root, 'lerna.json'));
const watchFolders = [];
lerna.packages.map(pkg => {
watchFolders.push(
...glob.sync(
pkg.slice(-1) !== '/' ? `${pkg}/` : pkg,
{cwd: root, realpath: true}
).filter(pkgPath => pkgPath !== __dirname &&
!pkgPath.includes('/node_modules'))
);
});
const extraNodeModules = new Proxy(
{},
{
get: (target, name) => {
const modulePath = path.join(
__dirname,
'node_modules',
...name.split('/'),
);
if (!fs.existsSync(modulePath)) {
return target[name];
}
return fs.realpathSync(modulePath);
},
},
);
module.exports = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: false,
},
}),
},
resolver: {
extraNodeModules,
},
watchFolders
};
Here's one more workaround for the record using yarn pack
cd ../my-react-native-library/
yarn pack // creates a .tgz file
cd ../my-project
yarn add ../my-react-native-library/generated-file.tgz
That worked for me π
@weslleyaraujo thats cool for temporary fixes, but if you want to edit those packages without repacking and readding constantly, it's going to be a bit of a pain.
Oh yes indeed, for local development it wont help.. small changes are fine π
@cpojer Consider this comment: https://github.com/facebook/metro/issues/1#issuecomment-481973752
@jdpace2 I run into interopRequireDefault from ../packages/imported-package after using your solution--its for the imported file.
@MANTENN I don't want to leave you hanging, but can you provide more context? I don't know what ../packages/imported-package refers to.
@MANTENN I don't want to leave you hanging, but can you provide more context? I don't know what
../packages/imported-packagerefers to.
It's an arbitrary name; this error appears when I import a package from a monorepo using this configuration. I assume inside that package, I would also need to have babel rather than have a single one to handle all
Imagine a folder structure like this:
When I import UI inside mobile, thats where the error is coming from.
@MANTENN the interopRequireDefault does seem like a babel problem. What does one of your imports look like in a Mobile component file, specifically how it references UI? I was able to resolve a lot of problems with that resolver and '@namespace/reusableModule' My import statements would look like:
import {someClass, anotherClass} from '@namespace/reusableModule';
@jdpace2 I grab the default export--I only have a single exported component.
Imported like this:
import Component from "@namespace/ui";
@namespaces/ui exports:
export default {};
Hey all - Is this still going to be a problem when 0.61 lands with the haste removal work?
@awgeorge I second your question. I hope this is resolved. I would love to get back on Metro and off haul.
As far as I can tell this still doesn't work in [email protected] (lerna monorepo with yarn workspaces). The messaging no longer includes references to haste, but the error is the same.
Unable to resolve module `redacted-lib` from `redacted-component`: redacted-lib could not be found within the project.
If you are sure the module exists, try these steps:
1. Clear watchman watches: watchman watch-del-all
2. Delete node_modules: rm -rf node_modules and run yarn install
3. Reset Metro's cache: yarn start --reset-cache
4. Remove the cache: rm -rf /tmp/metro-*
RCTFatal
__28-[RCTCxxBridge handleError:]_block_invoke
_dispatch_call_block_and_release
_dispatch_client_callout
_dispatch_main_queue_callback_4CF
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRunLoopRun
CFRunLoopRunSpecific
GSEventRunModal
UIApplicationMain
main
start
0x0
I followed @jgcmarins solution above and modified it for my usage.
I followed @liamjones config and add this to
metro.config.jsextraNodeModules: new Proxy({}, { get: (target, name) => path.join(process.cwd(), `node_modules/${name}`) })This solved for me.
I had a slightly different use case. I'm building reusable components for my company's react-native apps and I wanted to have a test RN app embedded in the module so I could test without having to publish.
I modified the above solution to work for me, and have included my complete
metro.config.js:let path = require('path'); module.exports = { transformer: { getTransformOptions: async () => ({ transform: { experimentalImportSupport: false, inlineRequires: false } }) }, resolver: { /* This configuration allows you to build React-Native modules and * test them without having to publish the module. Any exports provided * by your source should be added to the "target" parameter. Any import * not matched by a key in target will have to be located in the embedded * app's node_modules directory. */ extraNodeModules: new Proxy( /* The first argument to the Proxy constructor is passed as * "target" to the "get" method below. * Put the names of the libraries included in your reusable * module as they would be imported when the module is actually used. */ { '@namespace/reusableModule': path.resolve(__dirname, '../src/') }, { get: (target, name) => { if (target.hasOwnProperty(name)) { return target[name]; } return path.join(process.cwd(), `node_modules/${name}`); } } ) }, projectRoot: path.resolve(__dirname), watchFolders: [ path.resolve(__dirname, '../src') ] };
This worked for me, I just needed to add my module's node_modules to the watchFolders, i.e
watchFolders: [
path.resolve(__dirname, '../src'),
path.resolve(__dirname, '../node_modules'),
],
In my case where I have monorepo using yarn workspaces and one of the packages (example) requires all other (components) I was able to make it work by marking this example package as nohoist and adding this:
{
resolver: {
extraNodeModules: new Proxy(
{},
{
get: (target, name) => {
return path.join(__dirname, `node_modules/${name}`);
},
},
),
},
watchFolders: [path.resolve(__dirname, '../packages/')],
}
to its metro.config.js
Complete setup is here: https://github.com/pietile/pietile-native-kit
i just want to add once more, like @fdwcomune, adding watchFolders was enough.
For reference, here's my directory structure (I use yarn workspcaes):
βββ package.json
βββ packages
βΒ Β βββ api
βΒ Β βΒ Β βββ package.json
βΒ Β βΒ Β βββ scripts
βΒ Β βΒ Β βββ src
βΒ Β βΒ Β βββ tests
βΒ Β βΒ Β βββ yarn.lock
βΒ Β βββ app
βΒ Β βΒ Β βββ android
βΒ Β βΒ Β βββ ios
βΒ Β βΒ Β βββ package.json
βΒ Β βΒ Β βββ src
βΒ Β βΒ Β βββ tests
βΒ Β βΒ Β βββ yarn.lock
βΒ Β βββ types
βΒ Β βββ package.json
βββ yarn.lock
and here's my metro.config.js:
module.exports = {
transformer: {
babelTransformerPath: require.resolve('react-native-typescript-transformer'),
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: false,
},
}),
},
watchFolders: [path.resolve(__dirname, '../types')],
};
I tried lots of solutions in that past and I decided to try and check the updates on this issue and ended up having a simple solution to this problem
watchFolders was enough for us initially but we were using yarn link and not lerna or yarn workspaces so we still had to use get-dev-paths from @aleclarson to generate watchFolders.
We also ran into issues where we had a symlinked package that had react-native as a dev dependency (for tests) and metro/jest-haste-map didn't like that at all, falling with:
jest-haste-map: Haste module naming collision: react-native
The following files share their name; please adjust your hasteImpl:
* <rootDir>/node_modules/react-native/package.json
* <rootDir>/../../the-symlinked-module/node_modules/react-native/package.json
So we ended up also having to dynamically generate the resolver.blacklistRE based on the conflicting deps in order to finally get it to work.
We packaged it all up in a helper library that you can wrap your config in or use its bits and pieces individually: https://github.com/Carimus/metro-symlinked-deps
Thanks for all the info here, it helped me make a lot of progress but was wondering if i'm doing anything obviously wrong.
So far I haven't even been able to get the project to work correctly in the monorepo with workspaces (i haven't even bothered trying to import the shared module yet).
I have a monorepo with structure:
βββ package.json
βββ shared-module
βββ mobile-app
β βββ package.json
β βββ index.js
| βββ src
βββ yarn.lock
the root package.json has workspaces:
"workspaces": {
"packages": [
"shared-module",
"mobile-app"
],
"nohoist": [
"**/react-native",
"**/react-native/**"
]
With this setup starting the packager yields lots of errors because it doesn't resolve any of the packages that have been hoisted to ../node_modules.
Following suggestions above I added:
resolver: {
extraNodeModules: new Proxy(
{},
{
get: (target, name) => {
if (target.hasOwnProperty(name)) {
return target[name];
}
return path.join(process.cwd(), `node_modules/${name}`);
},
},
),
},
projectRoot: path.resolve(__dirname),
watchFolders: [
path.resolve(__dirname, '../node_modules'),
],
This results in a weird state where it resolves all the modules without error, but doesn't seem to be resolving the actual source of the app and just renders a white screen. Are there any other obvious changes i'm missing? Thanks again!
@johnryan
Yo! Hope things are going well.
last time I had to deal with this, i ended up getting lost in how expo did it. I bookmarked this https://github.com/expo/expo/tree/abf8ed28c38aa46db4e250d8fa8515c2eb69a23a/packages/expo-yarn-workspaces because it was helpful in figuring out the right configuration.
They are a couple of react native versions behind though.
@johnryan This is my answer https://github.com/facebook/react-native/issues/21310#issuecomment-540227031 from different issue describing the setup which worked for me for:
@johnryan in my react-native project, i tried to just nohoisting specific packages only initially but it's a pain because you have to include deps of deps and other stuff as well you'd want to end up just setting
"workspaces": {
"nohoist": [
"**"
]
},
inside react-native project's package.json.
It kinda defeats the purpose of having workspaces at first sight, but if you actually have more than 2 modules rather than just say an api and app, it's not.
@johnryan in my react-native project, i tried to just
nohoisting specific packages only initially but it's a pain because you have to include deps of deps and other stuff as well you'd want to end up just setting"workspaces": { "nohoist": [ "**" ] },inside react-native project's
package.json.It kinda defeats the purpose of having workspaces at first sight, but if you actually have more than 2 modules rather than just say an
apiandapp, it's not.
In my experience you just need to nohoist react native libs and then in some cases react - e.g. if you need multiple versions of react inside your monorepo and in the same time you are sharing hooks from a common folder (doesn't have to be a package).
nohoisting deps of deps and others is pretty straight forward, usually what I do when I add a native package (e.g. react-navigation)is:
"nohoist": [
"**/react-navigation",
"**/react-navigation/**"
]
What I'm trying to say is that hoisting is a good thing as long as you correctly specify your dependencies in child packages and don't lean on it in a sense:
"If my packages A and B depend on dependency D, then I will specify it only in A since it gets hoisted and B can get it from the root" => this would lead to lot of issues later. Always specify all dependencies in a child package and all is going to be fine (except some corner cases)
Thanks for all the help...what ended up working for me (in case anyone else ends up here) was:
root package.json
"nohoist": [
"mobile-app/**"
]
then in metro.config.js
const path = require('path');
module.exports = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: false,
},
}),
},
resolver: {
extraNodeModules: new Proxy(
{},
{
get: (target, name) => {
return path.join(__dirname, `node_modules/${name}`);
},
},
),
},
watchFolders: [
path.resolve(__dirname, '..'),
path.resolve(__dirname, '../node_modules'),
],
};
the watchFolders was the key piece for my packages. Since all my modules are in the root directory, resolving .. covers my monorepo packages and ../node_modules covers any packages that were hoisted from those deps into the root directory.
Hopefully this gets addressed at some point because there's a lot of required reading to get monorepos working in any fashion with react-native and it certainly doesn't behave "as expected"
I dealt with this for our own project in zulip/zulip-mobile@cc5d38211. The workaround is adequate, though like everyone I would love to see this fixed properly in Metro itself.
The central logic has much in common with others' workarounds above. My version includes comments on all the subtle bits I had to figure out; I hope they might be helpful for other people dealing with this issue too. Here are the key bits lightly edited, from metro.config.js:
// Packages we might apply `yarn link` to.
// TODO compute what packages actually *are* under `yarn link` instead.
const linkablePackages = [
'@zulip/shared',
];
/** Absolute path to the directory at the root of the given package. */
const packagePath = packageName =>
path.dirname(require.resolve(`${packageName}/package.json`));
/** Direct (not transitive) dependencies of the given package. */
const packageDeps = packageName =>
Object.keys(require(`${packageName}/package.json`).dependencies);
module.exports = {
// This causes Metro to even look outside the zulip-mobile tree in
// the first place.
watchFolders: linkablePackages.map(packagePath),
resolver: {
// These are to help Metro find modules imported from files found
// outside our tree. Without it, Metro tries to resolve them from
// the ancestor directories of those files and doesn't look in our
// own node_modules.
extraNodeModules: Object.fromEntries(
// @babel/runtime makes the list because our Babel config (?) causes
// files like @zulip/shared/js/typing_status.js to need it, whereas
// it's not a dependency of @zulip/shared itself.
['@babel/runtime', ...Array.flatMap(linkablePackages, packageDeps)].map(
name => [name, packagePath(name)],
),
),
},
// transformer: ...
};
The same commit also contains workarounds -- much simpler than this one, thankfully -- for similar issues in Jest and in Flow.
thanks everyone for the efforts resolving this, i have created a simple example with shared code between react & react-native inspired by your comments above β€οΈ
@Titozzz has written a blog post about React Native + Monorepo
https://twitter.com/titozzz/status/1216774222858653696
https://engineering.brigad.co/react-native-monorepos-code-sharing-f6c08172b417?gi=8573db4afef2
Thanks @jgcmarins, I remember reading that metro issue when it was first created haha. If anyone has questions on the article above, please feel free to DM me on twitter π
has anybody had success yarn linking a module outside of their monorepo?
has anybody had success
yarn linking a module outside of their monorepo?
@frankenthumbs the only way were able to accomplish that was with some modifications to our config that we packagafied here: https://www.npmjs.com/package/@carimus/metro-symlinked-deps
Any of you managed to use react-native-svg-transformer in the monorepo?
It's the only thing that doesn't work for me.
has anybody had success
yarn linking a module outside of their monorepo?@frankenthumbs the only way were able to accomplish that was with some modifications to our config that we packagafied here: https://www.npmjs.com/package/@carimus/metro-symlinked-deps
Works like a charm, kudos!
Any up to date for this? Still can't use symlinked modules.
Symlinked node_modules dir is the only way to deal with iCloud sync issues (renaming node_modules to node_modules.nosync then create a symlink node_modules@ -> node_modules.nosync).
Please, fix this issue :(
has anybody had success
yarn linking a module outside of their monorepo?@frankenthumbs the only way were able to accomplish that was with some modifications to our config that we packagafied here: https://www.npmjs.com/package/@carimus/metro-symlinked-deps
Works like a charm, kudos!
Yeah, I used it on a yarn link and it works well π€π»
When metro resolves a file it looks for the file in a jest-haste-map which also watches for changes to these files. There are two implementations for building a haste map (crawling the file tree from the project roots):
watchman native (c++, rust, etc.) library.Watchman
watchman doesn't support symlinks for 5 years. (https://github.com/facebook/watchman/issues/105).
Node impl
The node implementation deliberately doesn't follow symlinks for reasons unknown - maybe keeping parity with watchman?
_fs().default.lstat(file, (err, stat) => {
activeCalls--;
if (!err && stat && !stat.isSymbolicLink()) { <-- symlinks are just skipped
if (stat.isDirectory()) {
search(file);
} else {
A simple patch below would follow symlinks - but maybe other issues would arise because symlinks can point to files outside of the project roots.
//_fs().default.lstat(file, (err, stat) => {
_fs().default.stat(file, (err, stat) => {
activeCalls--;
//if (!err && stat && !stat.isSymbolicLink()) {
if (!err && stat) {
if (stat.isDirectory()) {
search(file);
} else {
@vjpr interesting - is this much different from the following PRs?
The Jest maintainers seem to have no sense of urgency around resolving this issue and do not seem interested in any solutions that only fix the problem for metro users without more robust handling for the larger jest ecosystem.
Guys, do you know how to use symlinked files? Not repos.
I have import file from 'path/to/symlinkedFile'
I am making a new ecosystem where I need to run the symlink component in the project. symlink are working with create-react-app with override some properties but it is not working on expo
@nomi9995 See https://github.com/facebook/metro/issues/1#issuecomment-309990663
The package bundler for Expo/RN (metro) doesn't follow symlinks, so you have to add them manually.
@kesha-antonov Add the parent dir of the symlinkedFile to getProjectRoots.
@nomi9995 See #1 (comment)
The package bundler for Expo/RN (
metro) doesn't follow symlinks, so you have to add them manually.
rn-cli.config.js
'use strict';
const path=require("path");
var config = {
getProjectRoots() {
return [
path.resolve(__dirname),
path.resolve(__dirname,'../../../Documents/FastTrackRN/testScreen')];
},
getAssetRoots() {
return [];
},
};
module.exports = config;
I made symlink testScreen into screen DIR and then import
import TestScreen from './screens/testScreen'
i am getting same error
Unable to resolve "./screens/testScreen" from "src/App.js"
@vjpr it is not working by manually
i am algo getting this error
β Validation Warning:
Unknown option "getProjectRoots" with value getProjectRoots() {
return [
path.resolve(__dirname),
path.resolve(__dirname,'../../../Documents/storytesting/FastTractRN/testScreen')];
} was found.
This is probably a typing mistake. Fixing it will remove this message.
@rafeca @davidaurelio @mjesun @jeanlauliac @cpojer and @haggholm can you help me in
import file from 'symlinkedFile'
any there to help me about import symlink
import file from 'symlinkedFile'
Locking this issue due to spam. Will reopen in the future.
Most helpful comment
Can we leave this open until it's resolved?
This is a huge PITA for code sharing, and developing modules. I'm not sure what your looking for in terms of a strong case, but it's definitely a major pain point for me, and module developers.
Currently I am trying to use lerna, and react-primitives to reorganize a project boilerplate to maximize code reuse, while maintaining upgradability by rn.
Lerna works by symlinking and installing your packages in a mono repo, which is an incredibly helpful pattern for code reuse, and completely broken when the react native packager won't follow symlinks.