-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Resolving dependency conflicts when importing node package typings #4665
Comments
I would appreciate feedback on this issue. Pinging a few folks who would be interested, @vladima, @RyanCavanaugh, @basarat, @poelstra, @johnnyreill, @jbrantly, @Zoltu, @weswigham and @mhfrantz. |
In the example case, does the MyUtils JavaScript library pollute the global namespace meaning there already is a conflict at the JavaScript level? If so, how does JavaScript handle the problem? Personally, I think as much as possible should be done to discourage global namespace pollution. Because of this, as long as there is a really good story for the non-polluting dependencies then I really don't care that much about what happens with the globally polluting story and to a certain extent, I want it to be a little painful.
My detest for global namespace pollution goes so far as to suggest that in the case of conflict, the user should be forced to manually resolve and no auto-resolution should occur. While this is annoying, it is less likely to result in odd run time or compile time behavior that will surprise the user. This will, hopefully, also encourage authors to stop polluting the global namespace (something they really shouldn't be doing in the first place). All of the above being said, I recognize that we are stuck compiling to a language that has a lot of existing libraries that pollute the global namespace so perhaps my stance is too harsh to be accepted by the community at large, or perhaps it simply isn't realistic to implement. |
Re-pinging @johnnyreilly since the first one was messed up 😄
It seems to me this still has the same issue pointed out here. Namely, if myutils had some kind of breaking change between 1.0 and 2.0, it's possible that mylib is using old typings that could cause a compilation failure. (Simple example: removal or rename of an interface). |
IMO, from a package consumer perspective, all module declarations should be scoped to the package they are declared in, unless they are explicitly exported into the context of the package which includes it. One of my dependencies' inclusion of an older version of a library shouldn't hinder my use of a newer one, and nor should my use of a newer one interact with the older one. Each package should be considered a unique scope whose only exports are those it explicitly chooses to export, just as with the underlying javascript. Global namespace pollution in the case of packages should be opt-in and considered very non-kosher, not "the natural way things are" using an existing feature. External modules are already scoped correctly for this, so the issue lies with ambient external modules. I believe that ambient external modules should be scoped to the package they are declared in to help alleviate transition issues, and that the ambient external module syntax should be discouraged for use with node style packages (but not an error, as described above). I do not believe that "versioned" dts are a good idea, given that npm is already doing version and dependency management for us. We should not be redoing it, we should be working frictionlessly with it, and supporting existing patterns. |
@weswigham I had the exact same thought. In fact I was writing up a proposal for it when you posted. I made it a separate issue (#4668) so as not to detract too much from discussion of the proposal here. |
I think and, I want to |
@mhegazy I was hoping for #2839 to get some love in 1.6, so thanks :)
Option 1 is indeed a good idea (and is what #2839 is basically about). But it still fails for deep (non-Typescript) dependency trees, which is where option 2 can be useful. Even more 'advanced' example dependency tree:
(Note: I assume none of the JS modules pollute the global namespace, i.e. they are 'normal' CommonJS modules.) In this case, because Option 2 could help with this, where it can reliably distinguish between What I'm missing from this proposal, though, is how the resolution and version-lookup logic would work exactly (e.g. like how #2338 searches node_modules and uses package.json). I'm going to type up what I think it could be doing in another post/issue. |
I've written a proposal for the lookup logic in a separate issue: #4673. Key take-aways from it:
|
The main problem is we do not know if a package is really polluting the global namespace or it is just authored this way because 1. it was the only way you can node typings to work before #4352, and 2. because it is convenient to write one typings file for multi-loader (aka isomorphic) packages. In an ideal world, all typings in a node application will be in in form of a proper external module, with no global namespace pollution, and each package would export its own declaration file. However, it is not realistic to assume that though, as 99.9% of typings today are authored as ambient external modules and not all dependencies will export their own typings, so package authors still need to re-export some .d.ts files with their package (ideally the ones already on definitely typed). If all typings are in a proper external module format, there is no conflict resolution to handle. you can have multiple packages depending on different versions of the same library, possibly with breaking changes, and everything would work, because of the external module scoping rules. The main problem we need to solve, is resolving the global scope conflicts. hence the version proposal; i really can not see how else to solve this. @jbrantly i believe this is the same problem you have today. if you are using tsd, and you have a transitive dependency on two versions of the same package, you need to "flatten" them, either by picking the latest, or manually modifying the typings to create a new typings file that is compatible both versions. i would expect this to continue being the solution here as well. One thing that i have not mentioned in this proposal, but is related, the importer, should not get errors from the package unless they have a way of witnessing it. i.e. in the example in the original post, if there is a missing/renamed declaration, in @poelstra, your proposal in #4673 is for locating a typings file, which is a fine proposal, but does not handle conflicts in the global namespace. if your @Zoltu, and @weswigham we can not just say no global name space pollution; though I do agree it is a bad practice. i think the best we can do is provide guidance. |
Since TS has structural typing, couldn't you nest the declarations from the dependent modules within the namespace of the module that uses them, and still have the interoperability that you would want for any exported portion of the interface? For example, if myLib uses myUtil, which in turn declares the Foo class, then it could be declared as myLib.myUtil.Foo. If myOtherLib uses a different version of myUtil which declares the Foo class, then it would be myOtherLib.myUtil.Foo. Any attempt to interoperate between myLib and myOtherLib via an exchange of Foo instances would work if they had the same structure. If they did not have the same structure, then that incompatibility stems from a structural incompatibility, and from not some artifact of the TS type system. The downside would be that you would define some modules repeatedly within each context they are used. That would increase the computational resource requirements for the compiler. |
@mhegazy I guess my point about versioning and picking only the latest copy is that it seems like only a halfway solution instead of a full solution. It fixes one problem (duplicate declarations) but leaves another (incompatible declarations). It would be ideal if both problems could be solved (which I believe is possible). |
I feel it is reasonable to scope the present ambient external module syntax to packages and then provide an explicit escape syntax to pollute the global environment. I think this is the easiest to transition to better practices with and the easiest to consume and have consumed packages "just work". |
If I have no version conflict with DefinitelyTyped typings, would it not be 'definition duplicate'? Like below dependencies tree:
Then, I can use it from now after a fashion. |
I believe this is already exactly what happens, when 'proper external' typings are used. |
This is a great idea, it would also allow 'proper external' modules to indicate that they do make stuff globally available (e.g. in case of a shimming library). |
Of course, but the way Node modules work, you'd have to 'try hard' to pollute the global scope (e.g. assign to a property on So it makes sense to (like @weswigham proposed) have to more explicitly indicate globals in the typing (e.g. This is not really a breaking change, btw, because people only have to add that explicit global syntax when both:
With the proposal I made (#4673), you have the benefit that you can keep using 'isomorphic typings' (i.e. with And if someone explicitly marks something as global, and you have another package that marks the same variable/type as global (which could be two different packages, not just I'm not sure that last one is something that can really be fixed. For example, what would happen when loading two incompatible Promise libraries, which both try to install themselves as global? Which one is actually going to 'win' at runtime, so which part of the global exports of the typings should 'win'? (Although the 'local' types probably still function perfectly.) The case with |
I wanted to point out some additional things after thinking on this today.
At risk of seeming overbearing I would really like to point people to #4668 which I believe provides a mechanism that solves all of these issues as it directly addresses the root issue of a package's dependencies being made global. |
Regarding the
Note that
|
The fact that normal definition files today declare globals as the On Mon, Sep 7, 2015, 10:55 AM James Brantly [email protected]
|
@jbrantly Why do you think it allows global leaks? The idea of it is that it puts all these 'globals' (e.g.
Yes, they do. But in most cases, this is not what they really do. It's just basically the only way to make isomorphic typings today. E.g. current So, I consider the However, suppose I do something like That last variant is where I proposed that Or maybe better, if a package actually tries to use such a global in a way that is not allowed by all definitions of that global. E.g. |
@weswigham Exactly :) |
@weswigham The problem with what you're suggesting is that it completely disregards the use of libraries as globals in the browser. If all that TypeScript did was node-style modules then I would agree. But library authors (or definition authors) need to provide typings generally both for node-style modules and browser globals. If using proper external modules you would either need to 1) duplicate the definitions between the global and the module versions or 2) write one definition as a global namespace and then reuse it in the module version, but this leaks the global currently. If there was a better way to easily go from module -> namespace (#2018) then this wouldn't really be an issue. For now I've suggested something like #4337 which (inadvertently) works really nicely with my proposal. I think the real issue isn't that definition files can make globals, but that dependencies of definitions files can inadvertently make globals. This is a subtle difference, but an important one.
I might have misunderstood this point when reading your proposal. It wasn't clear to me that in legacy mode all globals would be isolated, including something like this: // wtf.d.ts
declare var SomeGlobal: any;
// myutils.d.ts
/// <reference path="wtf.d.ts" />
declare module "myutils" {
...
} If that was your intent, then I think this line in your proposal "Don't expose any declared modules to global module namespace" is essentially a one-line redefinition of my proposal 😄 You'd literally be saying that all of the dependencies of the module are isolated, which is more or less what I'm saying. If that's the case then I think we're actually fairly closely aligned but with some disagreement about whether or not proper external modules are even necessary. BTW, the above example was what I meant when I said that your proposal can still leak globals.
Today using normal definition files you'd just reference "polyfill-promise.d.ts" (either explicitly or through some fancy-pants lookup). No new syntax needed.
I think this point is debatable. I don't think it's necessarily true that runtime would break, and this seems like it could be a real PITA to force TS to not throw errors even though you know that the runtime won't break. |
No, because in my proposal the "legacy mode" trick (bad name by now, "mixed mode", anyone?) is only applied in the commonjs/node-resolution-mode. Browser/AMD mode can still use what suits them best.
Yes, that was my intent. And the few packages that really wanted
Yeah, and that might be the more logical thing to do in this case.
Explained why/when this might break in the example in #4668 (comment) (bottom part of the post) and #4665 (comment) (last sentence).
Agreed, which is why I added the "Or maybe better, if a package actually tries to use such a global in a way that is not allowed by all definitions of that global" :) |
Agreed. Would like definitions files as they generally are today to still be first-class citizens.
I didn't really mean from the consumption perspective. I meant from the authoring perspective. "you would either need to 1) duplicate the definitions between the global and the module versions or 2) write one definition as a global namespace and then reuse it in the module version, but this leaks the global currently" However, if proper external modules also had the global isolation logic applied then the second option could be used without leaking globals.
Well then, cool 👍 So I feel like your proposal could maybe incorporate mine to flesh out that section (without the opt-in part). I'll post something in your proposal about this.
I don't know. I'm still on the fence about this. It's obviously impossible to know which global was actually applied from a typing perspective. I lean toward thinking it's best to just let the user control which global to apply by referencing the proper definition file rather than trying to figure out some fancy logic to merge the globals and try to warn, etc. shrug Not the hill I'll die on though. |
Is there anyone who are using this feature in a serious way? How are you handling the redefinition errors, not even the version conflict you are discussing here? |
This feature isn't really implemented yet (I don't believe) except for #4738. It seem like the work so far is just focusing on not allowing triple-slash references nor |
I'm about to be way more verbose than I've been in this issue and the related competing proposal in the past.
That's precisely why we need to find a way to continue allowing ambient external declarations while continuing to improve the package consumer use case. This is why I suggest package-scoping ambient declarations by default. When using Here's what I think. People want to reuse their handwritten browser definitions. We should allow ambient module declarations of the style declare module "foo" {
//...
} Within packages. And we should continue to disallow directly having ambient definitions like that at the entrypoint specified in the "typings" field of a "package.json". However, we could then allow (or encourage) the following: /// <reference path="foo.d.ts"/>
export * from "foo"; //pulls the "foo" package from the package scope and exports it to the caller (First, re-enabling triple-slash references in packages.) And all ambient declarations made within a package are scoped to only within that package - this way when you include a package, you only get members intentionally exported by the package. The ambient module "foo" isn't accessible to your child packages or your parent package - it's as if you declared the types inline in your file. That's the key here. This way my package can include and declare any number of Intentional modifications to the global scope aren't currently possible within external modules - that's a separate but related issue. It's also a comparatively rare use-case within packages, and is lower priority than allowing reuse of existing I am not a believer in having "version overrides" for If we accept package scopes as a possible solution we come to the problems with implementing this solution. This requires some somewhat serious work within the type checker, from what I'm told. We don't actually have a concept of a package scope - we have a global scope and file/block scopes. External modules work because we rely on file scopes to isolate everything. To add package scopes, we'd need to add another scope concept between globals and files, and it would only exist by inference (if a package is accessed via an import statement which reads a typings field from a package.json, then inject all global files accessed via that file into a package scope for that package instead, and any external modules inside that package scope access that package scope before the global scope when looking up global types). This is nontrivial by comparison to the proposed Undoubtedly, the way forward is to mirror your source module JS files (which are external modules) with external module |
The check in #4738 is to limit library authors from publishing libraries that may cause conflicts in the global namespace in the future. We have spent the last two design meeting discussing this issue. we have not managed to come up with a reasonably simple scoping rules that would fit the node semantics and not break existing declaration files. The main problem is majority of the .d.ts files as they are written today do not express intent of global pollution correctly. if we limit them all to a scope, what does that mean to mutations ot built in types, e.g. Array? do we have a copy of lib.d.ts for every module? should we allow these to be global pouters? what if they conflict... The ideal world is one where every package carries its own typings with it, and everything is an external module. but we are not there, and we need a way to allow package authors to take a dependency on the lodash's, the mongoose's etc.. I think the original proposal is so far the simplest and most realistic approach to handle this issue without either rewriting all definitelytyped, or the TypeScript compiler or both. We will try to get this in master as soon as possible to allow for easier package typing. we also need to document publishing typings. @vladima started a wiki page here: https://github.com/Microsoft/TypeScript/wiki/Typings-for-npm-packages, but there is more to be done. |
@weswigham Thanks for detailed comments.
I feel like we're pretty much on the same page, actually. I agree with everything you've said with the only possible caveat being...
I think they should be possible (because it's possible in JS-land) and that it's something we do need to figure out sooner rather than later since a large part of the point of this discussion is how to handle global conflicts. FWIW, #4668 pretty much spells out what you've said with two exceptions:
Yea, I figured as much.
I think this is really my biggest gripe (see #4337). I really hope that whatever solution is arrived at takes this into consideration.
Did you look at #4337 or the "mixed-mode" concept in #4673? If so, do you have any feedback on those?
It seems to me like #4337 mostly addresses this. It's opt-in and therefore backwards compatible. Limiting mutations to built-in types is a good thing unless the the application author wants to see those mutations, in which case they simply use the correct definition file which makes the mutations. Super simple (non-working) example: import { polyfill } from 'es6-promise'; // note that the definition file here does *not* make the global Promise
import 'es6-promise/polyfill'; // make the global in TS-land, could also be a tripleslash reference or in tsconfig.json
polyfill(); // make the global in JS-land or, for something like whatwg-fetch which really does just always make a global because it's browser-based: import "whatwg-fetch";
fetch(...); Note that if using a package-scoping concept, if whatwg-fetch was used for some reason as a dependency of a package it would not pollute that global down to the application.
¯_(ツ)_/¯ |
As guidance that would solve the issue, the problem is not enforceable by the compiler. and what i worry about is making it easier to create these types will leave end users to suffer. |
I saw #4877 and thought it related to this discussion a bit as it's a node module that creates a global. |
this does not seem to be needed now with the move to |
Hi, im not pretty sure what im' writing in correct issue, but i guess i have related issue.
i can't remove one of such definitions because they are dependencies from third part packages and they are installed (@types/isomorphic-fetch, @types/whatwg-fetch) own td's. ps. i used some packages from MS to create webparts with react and apollo-client to connect to graphql. Please does anyone know how to resolve this conflict? |
This issue is explained in details in #2839
Terminology
Through out this issue, i will be referring to different ways of declaring external modules, here are what i mean by them.
Ambient external module declaration
Module declarations of the form:
Proper external modules
The other way of writing a declaration of a module, i.e. a declaration file with a top level
import
orexport
; e.g.:Script file
A file that is not a module, i.e. does not include a top level
import
orexport
. it could however, include ambient external module declarations as described above.Background
With module resolution work done in #4154 and #4352, it is possible for node package authors to distribute typings with their packages. For these packages, users do not have to independently acquire the typings from definitely typed, but will just
import
the package, the the compiler will locate the typings in the package directory (eitherindex.d.ts
or by followingtypings
property inpackage.json
in the package directory).As noted in #2839, for a package author interested in distributing their typings with their package, there are dependencies that they need typings for; the most common would be
node.d.ts
, which is possibly referenced by each package.The way they dependency typing are authored today on definitely typed is using ambient external module declaration; this indicates that these typings exist in the global scope, and thus are susceptible to conflicts. The classic the diamond dependency issue (courtesy of @poelstra):
Now, if
myUtils.d.ts
uses ambient external module declaration, this is guaranteed to result in re-declaration errors.Proposal
When resolving an
import
target in a node module, it is an error to include files that are not either:For proper external modules, there are no conflicts expected, as external modules do no polute the global namespace.
For versioned files, referring to the same library, the compiler will follow a conflict resolution logic picking only the latest copy (as described by @basarat in #2839 (comment)). The files provided on the command line will be allowed to override this conflict resolution policy.
Versioned files
These are files declaring an identity, i.e. a name and a version. A versioned file is expected to contain a comment at the top of the form
/// <library name="myLibraryName" version="1.0.23.2-beta" />
Where the
name
field is optional, and if committed, the file name is used instead, and theversion
field follows the version definition in http://semver.org/The text was updated successfully, but these errors were encountered: