Skip to content
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

Peer dependencies #10356

Closed
KnorpelSenf opened this issue Apr 24, 2021 · 5 comments
Closed

Peer dependencies #10356

KnorpelSenf opened this issue Apr 24, 2021 · 5 comments
Labels
duplicate a duplicate of another issue

Comments

@KnorpelSenf
Copy link
Contributor

Background

I am the developer of an a new framework for writing Telegram bots (ref, but the purpose is irrelevant here). The core framework is extensible by plugins that can be published by other authors. As a result, I publish one core package to deno.land/x, and other people and me publish other packages around it.

Problem Description

While it would be desirable that plugins do not depend on the core module, this is not always possible. Some plugins rely on different parts of the core package (both runtime code and some complicated type transformations) that are also exposed to users, and it makes little sense to duplicate this logic into each plugin.

Even if it is possible to achieve duplicate only some parts of the core library, and let maybe users pass in the missing components, this is not always practical and can lead to unnecessarily complex code.

Right now, some of the plugins specify the grammY core package as a direct dependency, but this can lead to users downloading multiple versions of the same package.

How Node Solves It

For an npm package, this would be solved by making the core package a peer dependency with specified semver ranges that are supported. This pattern can be seen a lot in the npm ecosystem, and while peer deps are generally to be avoided, they sometimes have their place.

One of the Solutions

It would be nice to be able to specify semver ranges in import URLs, à la

import { Bot } from 'https://deno.land/x/grammy@v^0.4.0||^1.0.0/mod.ts'

but I don't mind the exact syntax being different.

I am by far not the first person to mention this, and it is neither flawless nor the only solution. However, despite some people commenting on the problem, I cannot find a promising suggestion on how to overcome this issue. I looked into ways of restructuring the plugin architecture, but I always ended up at the point that at least one of the plugin packages needed to refer to the core package in some way or another, or at least parts of it.

Please let me know if I overlooked a GitHub issue somewhere that discusses this. If that does not exist, what are you suggesting how to handle these situations?

@sno2
Copy link
Contributor

sno2 commented Apr 24, 2021

A solution that I was thinking about for this is to require the developers that are using your suite of modules to include an import map that specifies the version of the core module (grammy) depended on by all the others. After that, the submodules that depend on grammy must use the alias grammy instead of the actual file name. Now, the developer who is using all of these modules must pass in the following import map when they run their code to be able to resolve the location of the imports used in submodules:

// importmap.json
{
  "imports": {
    "grammy": "'https://deno.land/x/grammy@v1.0.0/mod.ts'",
  }
}
// main.ts - User's code

import {  Foo } from "grammy";
import { FooPlugin } from "https://deno.land/x/grammy-plugins-foo@v1.0.0/mod.ts";

const foo = new Foo([FooPlugin]);
/// https://deno.land/x/grammy@v1.0.0/mod.ts

export function coreLogic() {
  /* do stuff */
}

export class Foo {
  constructor(public plugins: Function[]) {}
}
/// https://deno.land/x/grammy-plugins-foo@v1.0.0/mod.ts

import { coreLogic } from "grammy";

export function FooPlugin() {
  coreLogic();
  /* do some other stuff */
}

Now, you can run the project via the following command:

deno run --import-map=importmap.json main.ts

Within the READMEs of the submodules it would be extremely important to include the versions that their module is compatible with and how a dev can setup the project.

Also, I want to clarify that you can include whatever url you want when importing your file as long as it ends in .ts or .js. Therefore, you just need something like the Deno Third Party modules registry to be able to parse that semver ranges in the url. It's not a problem that Deno itself should be solving IMO.

@kitsonk kitsonk added the duplicate a duplicate of another issue label Apr 24, 2021
@kitsonk
Copy link
Contributor

kitsonk commented Apr 24, 2021

The Deno CLI does not care what semantics a placed on the import specifier. That is up to the registry. There are several registries that already provide packages that way, like unpkg, snowpack, esm.sh, etc.

What you are suggesting is that you might want deno.land/x/ to support semver. That isn't an issue for the Deno CLI.

All that being said, this topic has been exhaustively been discussed before, so closing as duplicate of denoland/dotland#47, denoland/dotland#195, denoland/dotland#288, #4574, #6030 and others.

@kitsonk kitsonk closed this as completed Apr 24, 2021
@KnorpelSenf
Copy link
Contributor Author

Supporting semver in the website is tracked by https://github.com/denoland/deno_website2/issues/606 but it does not seem like anyone is eager to solve these problems.

There is now a thin wrapper around deno.land/x called DSR that solves this. It is written in Rust and the code is here.

Try clicking https://dsr.edjopato.de/grammy/0.x/mod.ts. It will parse the semver specifier and then redirect you to the correct latest 0.x version on deno.land/x. You can now just use these URLs in import specifiers.

@KnorpelSenf
Copy link
Contributor Author

closing as duplicate of denoland/dotland#47, denoland/dotland#195, denoland/dotland#288, #4574, #6030 and others

I fail to see how any of these issues provide a solution to this problem. I don't care how radical the solution is or if I have to single-handedly rewrite the entire ecosystem around my project. My problem persists.

I could accept it if we collectively decided to just ignore the problem, and be like “meh, we don't care about library creators, we care about application developers, and they can use import maps.” That would of course not be ideal, but at least we can agree that there's a problem and that we don't do anything about it.

What's much worse than that is how the Deno team handles this situation currently. Closing this issue as a duplicate of other closed issues makes no sense. It seems like you are either actively suppressing any discussions on how to make the current design work, or you are lacking any understanding of the challenges our organisation is facing. Both would be disappointing.

In case you are trying to silence us (out of fear we would question URL imports? No intention to do that), I don't think I can do much more here. Sorry.

In case you just think that there's an easy and obvious solution, and I'm simply not seeing it, I don't mind talking this out. Our issue persists for 1.5 years now and a few dozen engineers with an understanding of the problem have participated in the conversation. No one could point out a solution.

Meanwhile, the library consumers have to pay the price, as the only workaround we know causes compilation errors that are not obvious to fix. As a result, from their point of view, using Node has so much less complexity when it comes to dependency management. No one of us what's that to happen, eh? :)

@mechanoid
Copy link

A little late to the party. In general I would also like to have a proper solution to this problem. What I do is to provide a different interface to libs that work with externally provided dependencies.

Conceptionally I use smth like currying to provide my lib functionality after passing the peer Deps to it first.

myLib

export default (peerDep) => (options) => {
  // lib functionality
} 

usage in app

import peerDep from 'peer_dep'
import myLib from myLib
const m = myLib(peerDep)
//...

For typing I try to write keep the dependencies to the peer Deps written in a duck typing manner
so that really only the really used properties/methods are described.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
duplicate a duplicate of another issue
Projects
None yet
Development

No branches or pull requests

4 participants