Replies: 11 comments
-
Thanks for considering this. To the problems of flat node_modules, I would also add that a subdep can actually disappear. The bar can stop relying on baz and baz will disappear altogether from node_modules. (I mentioned this in pnpm’s strictness helps to avoid silly bugs.) Also, a relatively easy way to fix this on the npm side in v7 would be making the |
Beta Was this translation helpful? Give feedback.
-
Ah! Yes, that is true. It'd be sort of a less aggressive form of the "break package layout" approach. The hazard there might be that deduplication is valuable for people who build code for the front-end. You wouldn't want to have 10 copies of a shared frontend asset in your bundle. In fact, there have even been some suggestions to use a bower-style "deduplicate everything, manually resolve conflicts" type of approach. |
Beta Was this translation helpful? Give feedback.
-
But I also see value in things like |
Beta Was this translation helpful? Give feedback.
-
Note that eslint-plugin-import, included in the Airbnb eslint config, prevents this problem. |
Beta Was this translation helpful? Give feedback.
-
There's also a simple solution to fix this using symlinks.
As a result, dependencies will have access to all unlisted deps (via I am thinking about implementing such structure for pnpm (see pnpm/pnpm#1998). The only con of this solution is that it won't work with node's |
Beta Was this translation helpful? Give feedback.
-
Well, that is a simple solution to the implementation, assuming we want to take on the pain of making a breaking change in this area. But I do like that it puts the burden only on the developer using unlisted deps, rather than on users.
Yeah, but lots of stuff doesn't work with that. I suppose one way to thread the needle would be:
It'd work in preserve-symlinks mode, because it'd just look like the npm@2 style:
My main objection to something like this is that symbolic links are kind of a pita, especially for Windows support. We'd potentially break some use cases with ELOOP cycles, as well, if we're not careful. Of course, pnpm has solved all these problems, but it'd basically mean adopting that kind of approach and rewriting a lot of code in a different way. |
Beta Was this translation helpful? Give feedback.
-
Another possible approach:
All direct dependencies of the current project go in If This would be relatively easy to reify by just changing the
In that case, |
Beta Was this translation helpful? Give feedback.
-
That seems like a great solution, but would it require node resolver changes? Also, can you have a package named “node_modules” already, including a manually created folder? |
Beta Was this translation helpful? Give feedback.
-
Nope! that's what makes it a good solution :) |
Beta Was this translation helpful? Give feedback.
-
pnpm v4 does a similar thing when hoisting is enabled, but puts the nested
|
Beta Was this translation helpful? Give feedback.
-
This is a call for an RFC to address a problem. Trying out a new thing,
using issues here. Sort of a request for comments on a potential request
for comment :)
Prompted by discussion in #43
Situation
Given this stated dependency graph:
Where the code in
foo
callsrequire('baz')
, but does not list it as adependency.
In npm from version 3 onwards, the resulting install tree will look like:
so
require('baz')
infoo
's code will resolve to thebaz
being pulledin by
bar
.If we implement #43, then this has the
potential to get slightly worse, because
peerDependencies
will alwaysbe installed where the dependent can reach them.
In other words, while this install tree would be valid for regular
dependencies:
if
bar
has a peer dependency on baz, then it must be installed wherefoo
can resolve it, thus enforcing deduplication which makes unlisteddependencies possible.
Furthermore, pnpm installs using a different approach, which does not
support unlisted dependencies. In pnpm's install layout, the tree would
look something like (simplified):
In a pnpm installation, the code in
foo
lives in anode_modules
folderwhere
bar
is reachable, butbaz
is not reachable. Thus, the unlistedfoo -> baz
dependency will causefoo
to not function properly. (Note:pnpm's
--shamefully-flatten
configuration will create a tree whereunlisted deps are resolvable, but this is not the default.)
Beyond pnpm support, this presents some other problems.
bar
may upgrade to a new breaking change tobaz
, but in such a waythat it does not require a breaking change in
bar
. (Ie, a change toAPI that
bar
abstracts away.) This can breakfoo
's usage ofbaz
in unexpected ways without warning.
baz
's author may not realize thatfoo
is a user, and this informationcan be valuable for the authors of both
foo
andbaz
.However, authors may be resistant to making a code change for something
that "already works fine".
Options
The options fall into three general categories:
Do nothing. (Aka: "This is fine." 🔥 ☕ 🐶 🔥)
We can accept that unlisted deps are just a fact of life, and let the
community continue to deal with it. Document listing your deps as a best
practice, try to get people to learn why it's bad, etc. But otherwise, do
nothing.
One argument for this would be that unlisted deps aren't a particularly
severe hazard. I think that's a hard case to make, given the issues above.
Another argument is that this is a problem for pnpm, but not one that faces
npm, so it's not ours to fix. Again, hard case to make, because it's a
problem for pnpm that npm creates, so we probably ought to fix it.
Detection
We can attempt to detect unlisted deps either at install time, run time,
via an explicit command (like
npm doctor
ornpm ls
), or asynchronouslyon the registry.
Having been detected, we can provide an automatic mechanism to fix the
problem (
run 'npm install foo@version' to remove this warning
, or evennpm autoinstall
).Finally, once detected, unlisted dependencies cna be either a warning, or
crash the process.
Install Time
Detecting the use of
require()
in installed code is very challenging.Users can have code in any file in a project which calls
require()
(orimport
), which may even modify the module system, be transpiled, etc.Additionally, this is difficult when a program calls
require()
with avariable rather than a string literal.
ignore.
Run Time
After npm version 8, we can instrument the
tink
module loader to detectread access to a file in a package that is unlisted, but appears in the
package-lock.
We'd probably not want to do this in production (ie, when
NODE_ENV=production
or--production
config flag set). And, it's morelikely to be ignored if warnings are for dependencies deep in the tree
(since the user seeing the warning can't But, if warnings were printed
whenever users ran tests, they'd be in a position to fix the error right
there (especially if it caused a test failure).
be ignored by users.
Detect Explicitly
When the user runs
npm ls
ornpm doctor
, we could scan for unlisteddependencies. This would have all of the same drawbacks as detecting at
install time (ie, scanning code), but without automatically being run on a
regular basis. Comparing behavior of
npm audit
vs the quick-audits thatautomatically run on
npm install
, it would seem this is not an idealapproach.
If we explicitly detected unlisted deps on
npm test
(and only then), thenthis would have most of the benefits of detecting at run-time, without
production run time disruption.
Detect On The Registry
We scan packages on the registry, and nag users with emails or something
when their package references a dependency that's not listed.
exists, for detecting malware), users hate nag emails, additional
server-side work with little benefit to the registry provider, depends on
a single registry provider
Warning on Unlisted Deps
Print a warning when an unlisted dep is found. Treat it as a reason to
complain, but not a reason to crash.
if this works fine??" response that we get from nags about missing
fields in package.json.)
Error on Unlisted Deps
Treat it as an actual error, some or all of the time.
This is likely justifiable in an explicit build- or dev-time situation like
npm test
ornpm doctor
, but probably too aggressive to do innpm start
.If we detect unlisted deps in install, then we'd also have to decide
whether this is an offense worthy of canceling and rolling back the build,
or merely exit non-zero after the install completes.
Break Functionality with Package Installation Layout
We could adopt a package tree layout approach somewhat like
pnpm
, suchthat unlisted dependencies would no longer function.
While this would remove the "it works, why bother?" objection to fixing
it, it would also be extremely costly to implement, since we'd still need
to maintain support for the classic "deduped" package layout approach.
Additionally, since many packages using pnpm already set the
shamefully-flatten
config value, it would appear that doing this won'tactually make users change their behavior. It's possible that npm
dropping support for unlisted deps will cause them to take more notice, but
it's also likely that this will be viewed as a bug and routed around, or
that users will be so disrupted we'll be compelled to add a workaround,
thus defeating the point of this approach.
Beta Was this translation helpful? Give feedback.
All reactions