Using yarn link is always a difficult task when working on a project with peer dependencies (if you google "npm link and peer dependencies", you can find many references and bug report). With Shared Components, it is even more difficult due to the way Lerna hoist dependencies shared between the packages it holds.
In this section, I will describe the features we are trying to achieve with the yarn link, the solution we studied and why we decided to ask host project to add aliases in their webpack configuration.
Let's picture the following situation:
- P is a package from Shared Components
- H is the host application. H uses P.
- P has three dependencies, A, B and C.
- A is a peer dependency of P. To work properly, there can only be a single instance of A in the final bundle.
- B and C are both regular dependencies of P, but other packages in SharedComponents depends on B, while only P depends on C.
In a regular installation of P in H, the folder hierarchy would look like this:
H/
├── node_modules/
│ ├── A
│ └── P
│ ├── build/
│ └── node_modules/
│ ├── B
│ └── C
└── src/
And in Shared Components, this is how it would look like:
shared-components/
├── node_modules/
│ ├── A
│ └── B
└── packages/
└── P
├── build/
├── node_modules/
│ └── C
└── src/
In this situation, when building my project H, I want:
- To be able to compile my application bundle
- To use the code in
SharedComponents/packages/Pand not the one inH/node_modules/Pto do so (to test the changes I made there) - To use only the package A stored in
H/node_modulesand not the one inSharedComponents/node_modules(to make sure only one instance exists in my whole application)
By using only a yarn link without any further configuration, you remove the whole P folder from H/mode_modules and replace it by a link pointing to the one in SharedComponents/packages. When building your application, webpack will follow this link and resolve P's dependencies from its new location, which means from within SharedComponents. As a result, the package A will be retrieved from SharedComponents/node_modules, which is not good (because the rest of H will use H/node_modules/A, leading to two instances of the same package in final bundle, and that is forbidden).
By using aliases in webpack configuration, we can specify precisely where to retrieve A from, regardless of the path of the importing file.
config.resolve.alias['A']: path.resolve('node_modules', 'A')We tried several other approaches, but none were satisfying. Here is a short summary of all our other attempts.
By adding in H's webpack config a single option, webpack would not follow symlinks.
config.resolve.symlinks: falseThis would resolve the peer dependency problem and is the accepted solution for most open-source project. But in our case, this is not valid due to lerna hoisting.
When this option is active, webpack acts exactly as if the folder P from SharedComponents was copied into H/node_modules. The resulting file structure (in the eyes of webpack, not for real) looks like this:
H/
├── node_modules/
│ ├── A
│ └── P
│ ├── build/
│ ├── node_modules/
│ │ └── C
│ └── src/
└── src/
And as you can see, B is not available anymore, which means the project cannot be built.
To solve the issue mentionned above, I tried to build a different version of P's bundle, one in which all dependencies would be included (except for the peer dependencies). This way, it did not matter if B was available or not in H's node_modules, as it was already inside P's bundle. But when proof-testing this solution on Widgets, the build time of the bundle jumped from 3 seconds to 40 seconds. This was not acceptable, especially in a context where developers want to build P in "watch mode".
A working solution would be to install P in H's node_modules, build your local P package and then copy all built files within H/node_modules/P/build, leaving all dependencies available. This would work. But if you want to quickly test the impact of different modifications on P in your project, this might get very annoying (besides, this is the kind of situation were human mistakes are frequent).
In that case, you can no longer use yarn link so you are on your own. But creating a symlink from H/node_modules/P/build to SharedComponents/packages/P/build should work (but don't forget to add the config.resolve.symlinks: false option in webpack to avoid the peer dependency problem). However, if you added a new dependency to P, you will not be able to test your feature that way.
You can set up a local NPM repository where your local P would be uploaded after each build and then installed on your project
This is probably the closest you can get from the real workflow. However we have not tested this solution, so we can't know for sure how easy / hard it can be. Some documentation here.