Skip to content

[RRFC] Issue regarding npm-v7 peer-dependency behavior as described by RFC-0025-install-peer-deps and currently implemented in arborist and npm-v7-beta #204

@KilianKilmister

Description

@KilianKilmister

UPDATE:

Full-fledged RFC: link
PR has been filed: (#210)
POC implementation in arborist fork: link

Any further discussion should be situated un those.
getting ready to close this issue


The Issue

UPDATE: RFC

First Up

I'm generally very much in favour of what RFC-0025 proposes. This should not be taken as an argument against the proposal, but as a vital amendment to it.

Example

The way I see it, there are generally 3 major issues with peer-dependencies.

  1. projects with conservatively low set upper version limits
  2. arborits current semver-checks will not match any dev/beta version even if no upper limit (or no limit at all with *) is
    set.
  3. It could add to dependancy-hell by locking a package consumer into outdated versions of a package

Now this has been a personal annoyance since i started with node, and all i coud do about it is ignore it. But in the current (v7.0.0-beta.4) npm v7-beta, it causes arborist to throw an error during npm update. (it probably affects more than just that, but this is the issue i ran into just now)
Companions: issue in npm/cli and issue in arborist

As mentioned, i think RFC-0025 is confronting a real issue, but i must imagine there is a considerable amount of people who would face similar issues as i am.

To Expand on my Issue

Running an npm-v6 install in the project i have currently opened (which is very representative of my projects) will result in this message:

npm WARN tsutils@3.17.1 requires a peer of typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta but none is installed. You must install peer dependencies yourself.
npm WARN ts-node@8.10.2 requires a peer of typescript@>=2.7 but none is installed. You must install peer dependencies yourself.
npm WARN @typescript-eslint/eslint-plugin@2.34.0 requires a peer of eslint@^5.0.0 || ^6.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN @typescript-eslint/parser@2.34.0 requires a peer of eslint@^5.0.0 || ^6.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN eslint-plugin-react@7.14.3 requires a peer of eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN eslint-plugin-import@2.18.2 requires a peer of eslint@2.x- 6.x but none is installed. You must install peer dependencies yourself.

Going over them

  • tsutils: helper type library. has seen Zero activity in the past few months
  • ts-node: typescript-code-runner. no upper version limit, but is still a mismatch because i use typescript@next (currently: "^4.1.0-dev.20200815")
  • @typescript-eslint/eslint-plugin and @typescript-eslint/parser: eslint-v6.8 is actually in my dependancy-tree as a dep of standard/standardx and it's that module that actually uses these two plugins.
  • eslint-plugin-react and eslint-plugin-import: two unfulfilled peer-deps (these itself being 3-nodes deep) also used by standard|standardx. (plus: i have never used react anyways)

But what all of them have in common: I never had an issue with not meeting their peer deps, and have been using them extensively and for various edge-cases.

The absurdity

Now it might sound like this is a project with many dependancys. But on the contrary. Below are all the deps listed in my package.json. It's a whooping 13, only two of which are used at runtime. i essentially have 50% (6 of 13) of installed modules complain about peer-deps, with non of them actually having a valid reason for it and none of them even used at runtime, so it's only local, and does't affect any users of the package. It's straight up insanity.

{
  "devDependencies": {
    "@types/node": "^14.0.27",
    "@typescript-eslint/eslint-plugin": "^2.34.0",
    "@typescript-eslint/parser": "^2.34.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^7.7.0",
    "standardx": "^5.0.0",
    "ts-node": "^8.10.2",
    "ts-typing-util": "file:../GitHub/ts-typing-util",
    "tsutils": "^3.17.1",
    "type-fest": "^0.16.0",
    "typescript": "^4.0.0-beta"
  },
  "dependencies": {
    "@repeaterjs/repeater": "^3.0.3",
    "reflect-metadata": "^0.1.13"
  }
}

concluding

Thus far it's only been a (quite frankly not so) minor neusance. But with unresolved peers now causing issues with project-management, this has become a major problem for me.
Now the eslint thing isn't that much of a problem, altough i would very much like to use the latest versions of it when i'm using it programmatically instead of being forced to use a version that's 6 months and a semver-Major behind.

The real issue is with typescript. My codebases always use dev-versions, and because all my work is in next-gen and frontier stuff, i really depend on it to stay ontop and ahead of things.

Suggested Changes

In my personal opinion, there has to be some sort override that can be aplied on peer-deps (of both regular- and dev-deps)
Imagine some fully functional and relatively popular, but badly maintained module has a peer-dep version range in which a major security vulnerability was found.
It might be perfectly functional to use it with the updated version of the peer-dep, but the repo is abandoned and doesn't get the PRs merged. (inactive repos like that sadly arent uncommon and the amount of potential issues they can cause will only increase over time)

Specificly adressing RFC-0025

I very much agree that the behaviour described should be the default. I believe it will improve the eco-system and will make it easier for people unfamiliar with node to understand what's going on. The current state of peer deps is a regular point of confusion, not just for newcomers, but still very much for a large number of active developers. Because as mentioned in the RFC, peer deps have regularly been used in an optional fashion or migrated to proper (optional-)dep because of user-confusion.

This paragraph is the one i have a massive problem with:

If D and P cannot be placed in the tree in the presence of the newly requested dependency, then refuse to install it until the user resolves the conflict. Otherwise, move D and P to their new homes as part of the installation.

As this behaviour would affect every project i worked on in the past 6 months, and without a way to override it, could make it impossible to use the current core of my dev-tools.

What I would suggest is somewhat similar to Alternative F (which in itself is less of an alternative as it is an amendment). I don't think that the author should specify which peer-deps get auto installed and which don't. This could possibly lead to similar user-confusion and resulting (re-)moval of peer-deps as we are seing it now. I think the descision to include a peer-dep should be declarative, and being able to make a peer-dep defacto optional defeats the purpouse, as it could just aswell be filed as an optional dep with a not in the projects readme.

Instead i would suggest to give the package-consumer the possibility to override their dependencies peers in the their package.json.

Implementation

I can imagine some serious issues in module resolution if a peer-dep could simply be flagged to be ignored by arborist. Instead i think this needs to be implemented in a way where arborist is handed an alternative module that it will treated as a valid peer by design.

From the top of my head i can think of a few ways to implement this using my deps as an example:
(i'm using the same key as in Alternative F just because. it could be whatever)

// the consumers package.json
{
  "devDependencies": {
    "ts-node": "^8.10.1", // has a peer of typescript@>=2.7
    "typescript": "^4.1.0-dev.20200815" // doesn't pass the semver-check because is a prerelease
  },
  "peerDependenciesMeta": {
    // tell arborist to settle for the locally install version
    "ts-node": { "typescript": "local" }
    // a hard override
    "tsutils": { "typescript": "^4.0.0-beta" } // possibly allow all the usual install specifiers like 'path' etc.
    // offer a substitute package
    "@typescript-eslint/parser": { 
      "eslint": { "standardx": "^5.0.0" }
    }
  }
}

NOTE: this should only affect module resolution. All changes caused by this should be the sole responsibility of the person who wants to override a peer-dep. the packages specified/linked would just be treated as a regular (dev-)dep and installed with the package (if the peer belongs to a regular dep with the finished package, if it belongs to a dev-dep then together with the other dev-deps)., so subsequent consumers would not have to worry about (or even have to be aware of) these mapping

Benefits

In my opinion this does come with a couple of advantages:

  • on the user side:
    • It gives the user more controle about what package-version to use (which matches the original goal of peer-deps)
    • it protects the user from having to rely on potentially outdated peer-deps
    • it enables the user to specify monkey-patched or wrapped version of the peer-dep that better fits his usecase (this one could have some great potential)
    • regular installing of peers would still be the default and the eco-system would recieve the same benefits as with the
      original RFC
    • Overriding is 100% opt-in by the user
  • on the implementation side: (i checked out the codebase and i'm quite certain about these points)
    • it would only be a minor change to Arborist and npm. all thats needed would be a bit of extra flow controle, and the overrides could resolved before the actual tree-creation starts
    • all the functionality already is present; module resolution is obviously already implemented and Arborist is already aware of wether something is a dev-dep or not

Conclusion

I believe my suggested changes would not only solve potential issues of the original RFC, they also persue the same goal, while allowing for more freedom and potentially even increasing the creative possibilities.
Additionally, the changes require minimal effort to implement and would be essentially unnoticable for the average end-user

References

Companion Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions