-
Notifications
You must be signed in to change notification settings - Fork 240
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Singleton Packages RFC. #23
base: main
Are you sure you want to change the base?
Conversation
This is something that we (Yarn) are interested in supporting as well. My comments:
|
There has been a related discussion some years ago. |
@arcanis : your last point reminded me of this discussion on Scoped Custom Element Registries, which has some support. If such scoped element registry becomes possible, the scenario of a component that defines a custom registry scope and a dependency on a component that, which today, must only be "defined" once, then an approach that makes these components unique to the whole dependency tree could be problematic. |
Another discussion of this topic happened as well. It seems there are multiple of these. |
I remember - the suggestion to use peer dependencies to define singletons looked interesting (but unfortunately relies on the ecosystem doing the right thing, which is a assumption that's been proven wrong multiple times). I wonder if the |
Thanks for feedback and questions, @arcanis.
I am assuming that package managers already produce an effective union of semver constraints such that transitive dependencies on I'm working on separate RFC to encourage
I agree. Optimized package semver union resolutioning, if not a thing, should be a thing that is separate from this thing.
I am going to have to get more familiar with Yarn workspaces. I'm quite familiar with Lerna but also haven't used it in conjunction with workspaces. Will explore. |
Just to chip in briefly here: Rust and Cargo have this distinction; https://doc.rust-lang.org/stable/cargo/reference/build-scripts.html#the-links-manifest-key It works well, but there's one big issue: this means that major version bumps of these kinds of libraries split the ecosystem. When those are big, foundational libraries, it can lead to a lot of pain. In Rust, this has mostly manifested as the The JavaScript ecosystem may not have situations like this, but I figured it was worth mentioning. Happy to answer any specific questions about it. Good luck! |
The JS ecosystem definitely already does - React, eslint, babel, etc - and it's generally handled by every participant in that "singleton" ecosystem adding a peer dep on it. (not that it's the best solution) |
It's a new year 🎉 |
a friendly reminder if there is anything we can do to help? |
Hey y'all! Thanks a lot for submitting this! Overall, we think this is a good idea. This feature does require us to land #27's implementation first, imo. Our plan right now is to land that implementation, see how it feels, and then see about this RFC and implementing it. Historically, npm has been a package manager that worked pretty hard to guarantee that you wouldn't run into dependency hell[1], and a feature like this one is a major step back into that hole (after we stopped installing peerDependencies by itself, which sort-of-kind-of pushed out the issue). If we're gonna introduce dependency hell into npm, I really want to make sure we have a good way to resolve conflicts. I appreciate the discussion that's been going on in this thread, and I hope it keeps going while we take the necessary steps to accept it. Thanks everyone for adding the context, and thanks again to @usergenic for taking the time to write and submit it. Hang in there, we'll get this in, and I think this looks a lot like what we'll end up accepting in the end, if not exactly like it! [1]: In npm parlance, dependency hell is when you have two different packages that require the same package at incompatible versions, a problem that's universal to package managers and that npm resolves through dependency nesting. |
@darcyclarke @zkat What's the latest on this particular one? This concept looks like a viable replacement to peer dependencies, which we're struggling to rationalize in various places across Microsoft and the oss ecosystem. There are some basic things we see:
The current answer for reducing duplication is to use peerDependencies. This only allows middle layer libraries a way to opt out of duplicating. But it isn't enforced well, and the strict mode work to error on unmet peers is making it even more difficult to understand who is at fault, when to use correctly, and how to reconcile. This RFC would help us eliminate peer dependencies as a concept. React could say: "I expect to be a singleton." NPM installs could error when React gets duplicated, and could provide guidance on the best course of action. Apps would have a way to override that when they disagree with default policies. Things would make sense. Very few people really understand peers but I think most would understand singleton declarations. I would love to see peers deprecated in favor of this. |
I would offer a suggestion to improve the feature; in addition to a singleton flag on producer package.json definitions, I would also allow consumers to override that default. There are edge cases where it's ok to go against the producer's recommendation but they need to be intentional. It might look something like this: dependencyPolicies: [
// allow duplication of react
{ name: ["react", "react-dom"], policy: "default" },
// prevent duplication of mui, wildcard support
{ name: "@mui/*", policy: "singleton" }
] React might be a case where it declares itself a singleton. It's the most used peer dependency I know. However some cases could allow multiple react instances to live in the dependency graph. E.g. test scenarios. D3 is bulky and might want to be a singleton by default. However, some pages might not care about bulkiness or are not using the full library, so the duplication might be acceptable. When violations occur, one viable suggestion might be to override the policy through this mechanism. (E.g., if |
Fwiw, Yarn supports this use case; package authors can list a dependency as both a regular dependency AND a peer dependency. When that happens, Yarn prefers the peer dependency if fulfilled, but silently fallbacks to the regular dependency if not. So in your case, it'd look like: {
"dependencies": {
"d3": "^x.y.z"
},
"peerDependencies": {
"d3": "*"
}
} We have thought about Singleton dependencies, but so far decided it wouldn't be a good strategy for us. Such packages would affect every single node from the dependency tree, which I'm worried would lead to irreconcilable version conflicts - unlikely in regular projects, but much more likely in monorepos. If someone really wants a singleton (but, really, peerDependencies are enough if used well), then overrides via the |
Hey @arcanis thanks for the insight and the link, I really appreciate your thoughts here! You described singletons and peers well. I would add a few comments. I would consider peers a reused workaround to an early design limitation. They made sense in npm 2.x where they provided a workaround for installing dependencies in a predictable path location relative to another package. They were designed for plugin scenarios (e.g. grunt plugins.) Later when the resolution and installation logic was changed to flatten the node_modules structure more, they were repurposed into what they are today: a non-obvious way for consumers to (optionally) opt out of the final resolution, but still provide semver requirements which generate warnings (or errors with the right flag set) when those expectations are unmet... all in hopes that the app can decide on the final single version of a dependency. And, it's a naively hopeful idea - there is no guarantee that all consumers in the final app's dependency graph will use them correctly. All it takes is for 1 thing to list I think we should also consider that peers are consumer-only metadata, yet producers are the ones who own their code as it changes over time, and whether it's safe for duplication or not. Consider the But, there are indeed scenarios where consumers want singletons for things that technically would be safe to duplicate, and primarily this is about controlling bundle size. This is the only reason why the argument for peers in their current form makes sense to me. We want 1 copy of react because we don't want to bloat the bundle size, and because we're likely to hit issues with the setup. However what doesn't make sense is to call this concept "peer dependency". This made sense when we were talking about folder structure peers in npm 2.x. The term "peer dependency" hasn't really explained expectations since (e.g. still don't know how to fix peers 😆) We should use vocabulary that explains the purpose and concept and have tools that do the right thing by default (e.g. This is where I believe we have an opportunity to do better by providing a mechanism with explicit vocabulary that formally solves the need to deduplicate in a clear way that can be defined at the producer level, overridden by the consumer, and enforced by the package manager. We are considering trialing this idea within our dependency graphs across some of our large repos. Basically we'd add policy data to our packages, write a separate prototype tool which traverses the install graph post-install, reads policy metadata, and fails on violations. This would help us learn about edge cases and where things don't work. We have multiple yarn monorepos with 2000+ source packages to try this in (mixing both react and react-native projects, which adds to the complexity.) If it works well, then my hope would be that we could move this concept into the package managers to enforce. It would be much more efficient since the graph and definitions are already parsed. I would definitely welcome your thoughts and ideas. Thanks again for the response and background. |
Peers make sense for any ecosystem with a core - eslint, babel, webpack, typescript, react, etc. It declares compatibility, and does already require something to be a singleton - at least within the highest subgraph that declares it. |
Yes, I agree. However, my take is different: I think we should do better to educate people and to have sane default, not abandon peer dependencies. It's unfortunately more difficult for npm to do that (because of the semantic changes they made around peer dependencies in npm 8), but that's the approach we want to take with Yarn. For instance, I think it would be acceptable to have a
Yes ... in most cases. But if there's one thing I'm sure, there are many use cases, and some of them rely on keeping the dependency tree subgraphs estranged 🙂 To give you an example, I myself have a third-party tool, which you install as a dependency in your project, and which lists |
preferPeer would be just as problematic as preferDev is - there does not exist a package that is always a peer, a dev, or a runtime dep. Everything has use cases as every category. |
Right, hence why those settings are suggestions, but not requirements. Those who won't use those packages the ninety-nine-percentil-way exist (as I said in my post I'm even one of them!), but are more likely than not to be advanced enough to know that the dependency must then be moved in |
Responding to both above:
I think singleton policies make sense. But the word "peer" doesn't indicate that's the intent to an average developer - to avoid introducing duplication. The vocab is part of the confusion (the lack of enforcement and configurability is the other part.)
Good food for thought and I see your point. Let's say react declared itself as a singleton.
Developer installs YourTool within App as a dependency. Package manager complains about singleton violation, explaining the options to the developer:
This feels better than blindly taking on a duplicate when typical intent is to deduplicate. In your case, it's fine to override the default because your scenarios are sandboxed; your app does not host YourTool within the same react-managed dom node on a single page. This experience would be so much more clear to explain to users. They would get a resolvable problem when they create dupes for things not intended to live multiple times in the same install. Sandboxed cases are the edge case where overriding policy makes sense. If 2 separate parts of a page both render separately with React, it's possible, but not desirable, to duplicate react. So to me it seems that producers should pick a best-case-scenario policy as the default. (Which usually means duplication is fine.) I can think of edge cases. What about when your App has a mix of middle layer libraries which should dedupe react, along with YourTool which are ok to duplicate? This might need to be something that policy overrides consider. E.g. exemptions may need to be specific to a particular relationship. Even with the edge cases though, it still seems more easier to grok by using clear language to define expectations. Flags like Ultimately we see this question asked over and over: should I list this dependency as a "peerDependency" or "dependency"? The answer is always vague. The real question is: is it ok to duplicate this dependency? Different scenarios require different answers. I stumbled upon this issue in material-table as an example: Reflecting on this... why was mui listed as a dependency in the first place? Why did they change it to a peer dep, and did they change it for the right reasons? Why didn't they change other things in their list to peers which might be dangerous to duplicate? (like This happens quite often across the ecosystem. We hear it internally at Microsoft with our own component libraries and utility packages. We want developers to do the right thing by default, without ambiguity, and with rails to guide them when they need to deviate from the common path. |
I've summarized my thoughts on how to address peer dependencies here: https://hackmd.io/@dzearing/BJifNnpsq The important parts are at the bottom but the TLDR:
This can all live side by side with peers so there isn't a conflict but if accepted and integrated, I think it covers a superset of scenarios and has some nice graceful fail behaviors that end users would appreciate, and at that point peers could be deprecated. |
Suggested feature for support of a
singleton
property inpackage.json
to guard against installation of multiple versions/instances of a package the package tree.Anticipates, but does not define course for resolving version conflicts among singleton packages. (That would be a separate RFC.)