My preferred fix for Yarn dependency tracking is to use zero installs[1] as there is no command to run and dependencies can be exactly what ships in the repo and nothing more (with the right flags). If accepting PRs from third-parties, the check-cache flag can run in CI to validate checksums from untrusted contributors — plus, you know, reading dependency source code when you have the time or reason to do so would be great within a PR review also.
I wish more tools were able to concisely show you the differences between dependencies, but… sometimes dependencies have binaries and at that point you might need to fork or clone and build your own version of a dependency. I’d suggest only using dependencies when you can read and understand the code, but there are always limits. I can’t think of the last time I thought of a glibc dependency except when using Alpine or compiling something for Cgo. But it’s still something to keep in mind: that sometimes your project will be simpler and easier with fewer, smaller dependencies where you can read the code in full.
Great tips for application developers. Though this still exposes you to potential transitive dependency supply-chain attacks on first-time installs, though it's likely very low risk at this point, especially if you're careful as you've described.
For library authors that want to take reasonable precautions yarn makes it very hard. Though some suggest leaving transitive dependency updates up to the consumer makes yarn more secure, as they can update to the fixed packages sooner..
but I don't believe this benefit actually manifests often.
I guess the moral of the story is that semver is really great on paper, but people suck at it, sometimes on purpose.
In the extreme case, though, a library author can run their own build toolchain and create an executable npm module with zero declared dependencies, then version that as many times as they desire as long as their test cases guarantee that the new build still passes and dependencies have been thoroughly checked.
By publishing the source code to git, those who desire to use a specific version could alternately embed the git URL in their dependencies instead of the pre-built npm package.
Yarn Pack command can be used to run a build command[1] and Yarn Publish[2] in turn calls Yarn Pack, if I understand this correctly.
That said… I agree that further work might be necessary here. I remember the first time I built an npm package and was confused when my lockfile in the package was completely ignored for dependency resolution. I don’t think this problem has gotten much attention, but the solution above might substantially solve the problem, if your dependencies are small enough to embed easily. It’s not ideal though, and I’d welcome other perspectives.
We do this for Ganache, except for 6 direct dependencies not authored by us, for some technical reasons. We do it twice as we ship a browser version and a node version. Our bundle size is not exactly ideal because of it. We'll be reducing our bundle size (the tarball downloaded from npm) soon and will likely increase it again shortly after by bundling these last 6 too.
Perhaps there’s an alternative to reduce the bundle size further: use code splitting or something like the shared module bundling that Webpack does for you when you have multiple entrypoints relying on the same shared code (if over a threshold minimum size) to reduce how much duplicated code is shipped multiple times for different entrypoints (e.g. uses or platforms or pages).
That said, the miracle of compression (and Yarn PnP not needing to uncompress) means you could have duplicate code and it won’t cost you much at all.
Another option would be to maintain your own dependencies by code splitting at the npm module level, but that could be considered an API-breaking change, I suppose, if the goal is to reduce how much is distributed by splitting out platforms.
You could also publish one layer of dependencies, probably, and still have exact versions pinned, but that would require maintaining CI build tools for your dependencies to ensure they are entirely built with no further dependencies or relying on republishing prebuilt no-dependency binaries in your own namespace.
I am reminded of how before tree-shaking became commonplace, it was routine to export libraries as lots of tiny npm packages, one per function sometimes, and import just the functions you needed as their own packages. That was taking it too far, and ECMAScript Modules (ESM) standards have somewhat replaced it, though for best tree-shaking you have to ensure your JS is actually modular and has no global state or side-effects when importing, which means you end up breaking some JS code when turning on Closure Compiler’s advanced mode, for example.
But we’re deep in the build optimization weeds now… the big advantage to pre-compiling libraries though is that you don’t have to tell others what build toolchain to use and instead provide standard ES5, ES6 or whatnot, already pre-compiled and ready for use (or… maybe, further tree-shaking…)
I wish more tools were able to concisely show you the differences between dependencies, but… sometimes dependencies have binaries and at that point you might need to fork or clone and build your own version of a dependency. I’d suggest only using dependencies when you can read and understand the code, but there are always limits. I can’t think of the last time I thought of a glibc dependency except when using Alpine or compiling something for Cgo. But it’s still something to keep in mind: that sometimes your project will be simpler and easier with fewer, smaller dependencies where you can read the code in full.
1: https://yarnpkg.com/features/zero-installs#does-it-have-secu...