Is there a way to tell lerna/npm to 'really look in my own node_modules for those peer dependencies'?
Asked Answered
I

1

6

I have a fairly standard lerna monorepo that will look like this:

packages/
    main/  - This is the main deployable application, it depends on both dep and react-dep
    dep/   - Just some pure functions, no problems here
    react-dep/ - A design system built with react, this is where we have problems. 

So a really common problem that happens, is as soon as you start using hooks in your dependency library, you get this message:

Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/warnings/invalid-hook-call-warning.html for tips about how to debug and fix this problem.

Which is because there are two version of React in your app, one from the main application, and one from the dependency.

Now - a common solution, that I've used, and works, is to declare react and any other shared/peer dependencies as externals in your webpack configuration. Eg, as suggested here. or look at this Github issues thread from react.

However, I'm not a fan of this solution, firstly, what if I'm not using webpack, and secondly I shouldn't have to manually keep track of which dependencies I need to mark as external.

I think what should work is:

In react-dep I declare react in both devDependencies and peerDependencies. The reason I put it in devDependencies is because my dependency library is likely using storybook or similar to develop the components, so I do need react around in development.

I think that this should work if I'm publishing react-dep to npm and consuming the compiled code from npm in main, because only dependencies are going to be fetched.

However, I think due to the lerna symlinking, what happens in this case is that the dev dependency is still around and we get this error.

Is there a way to solve this issue for a lerna monorepo?

Here's github repo that demonstrates this issue: https://github.com/dwjohnston/lerna-react-monorepo

Iives answered 28/5, 2020 at 1:43 Comment(5)
Just diving into the lerna issues, this thread seems like a good start: github.com/lerna/lerna/issues/375Iives
Best I've got for now, is to investigate whether yarn deals with this issue better.Iives
This lerna issue exactly asks this question: github.com/lerna/lerna/issues/1321Iives
Created this github issue here: github.com/lerna/lerna/issues/2605Iives
This stackoverflow question also kind of addresses the issue: #58027693Iives
A
2

As I see this problem potentially can be solved using lerna, npm, yarn or webpack.
I want to propose one more webpack solution there, opened a pr to your repo. If a webpack solution is a bad fit for you - just ignore this answer.

It is a little better than externals mechanism, because it will track overlapping peer dependencies automatically.

module.exports = function(config) {
    config.plugins.push(
        new NormalModuleReplacementPlugin(re, function(resource) {
            // first, remove babel and other loaders paths
            const requestWithoutLoaderMeta = resource.request.split('!');
            const requestPath = requestWithoutLoaderMeta.length && requestWithoutLoaderMeta[requestWithoutLoaderMeta.length - 1];

            if (requestPath) {
                // looking for a dependency and package names
                const packagesPath = resolve(__dirname, '../') + '/';
                const requestPathRel = requestPath.replace(packagesPath, '');
                const [packageName, _, depName] = requestPathRel.split('/');

                // if the main package has this dependency already - just use it
                if (dependencies[packageName]) {
                    console.log('\x1b[35m%s\x1b[0m', `[REPLACEMENT]: using dependency <${depName}> from package [main] instead of [${packageName}]`);
                    resource.request = resource.request.replace(`${packageName}/node_modules/${depName}`, `main/node_modules/${depName}`)
                }
            }
        })
    );

    return config;
}

This code will resolve your peer dependencies from main package using webpack.NormalModuleReplacementPlugin.

Note about webpack: since all three frontend kings use it in their CLI (angular, react, vue) I think you can use it easily and safely for such customizations.

I'm happy to hear about alternative technologies (eg. yarn) to solve this problem.

Give @nrwl/nx a try for your next monorepo instead of lerna.
The main difference there is that nrwl projects usually have ONE package.json only (using Google experience), so you need to install the dependencies once for all packages and will not face the problem you described.

Anal answered 2/6, 2020 at 12:23 Comment(2)
Thanks, I appreciate the effort. My current project does use webpack so for my particular case this might work, although I am still looking for answers about the state of play generally.Iives
What I like about this solution is that you don't need to manually declare each of the externals indvidually.Iives

© 2022 - 2024 — McMap. All rights reserved.