-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Design Meeting Notes, 11/22/2019 #35589
Comments
I have seen this several times but haven't seen details about it. Can someone help explain it for us plebs why this is "100% off the table, never to be considered, never to be discussed, never to be brought up"? I suspect there is a good reason for this, but it would really help me accept it if I could read the discussion that lead to such a hardline stance. I understand it is hard, but given the other options it feels like we really should be exploring all possibilities fully, including hard ones. |
Agree, if not rewriting the extension name, then how to emit valid code for Node module resolution and deno? |
Let's say you write some code in a JavaScript file var x = y + z; If you paste that code into a TypeScript file, it means var x = y + z; This the fundamental promise of TypeScript: You can take some JavaScript code, put it in a TypeScript file, and it means the same thing meant before. I would argue it is the most important design principle we have, because it's the only reason JS -> TS migration is possible and the only reason JS <-> TS interop is sane. This is why we don't have extension methods, even though it would be nice. We've kept that promise on literally any random JS snippet you can think of, and while some JS code may have type errors, it does the same thing at runtime it does before. Changing your file extension from Now you can say "Well that promise sucks, you should break it and just rewrite paths because rewriting paths is the best possible thing a programming language can provide". OK, fine. Our stance has always been that you should write the import path you want to appear in the emitted code, and add path mapping or other configuration to make TypeScript understand what that path should resolve to. This is 100.0% consistent with the idea that you should write the JavaScript you want to appear in the emitted code and add type annotations or assertions to make TypeScript understand what the type intent of the code is. In fact, it's not even a different principle at all, because an The thing I see every time is that people go through this cycle:
The right fix is to either:
TypeScript isn't here to provide a module aliasing system. If your loader supports such a system, great, use path mapping to describe it. TypeScript isn't obligated to invent new ways of adding epicycles to module resolution, and doing so would violate one of our core principles. But again, let's say you think that rule is bad. What happens if we start rewriting import paths tomorrow? Well, today, you write the import path that you want to appear in the emitted JavaScript file. You have two paths to consider here:
This is already insanely complicated. Tomorrow, if you write an import path, you now have three things under consideration:
This is taking a complicated 2-dimensional math problem and moving it to 3 dimensions. People can already barely understand the interaction of That said, this is 0% about the difficulty of such a system. The existing resolution system is already difficult; we are not afraid of doing difficult things. We are opposed to doing things that violate our fundamental promise that the JS code you wrote is the JS code you get. If you find yourself trying to write a valid runtime path, and can't get TypeScript to understand how to resolve that path to the target module, tell us about it. We have half a dozen module resolution flags and will keep adding more until any valid runtime path you write can be reasonably resolved to a corresponding file or declaration. Conversely, don't tell us that you wrote an invalid runtime path and want us to fix it for you! This is the same as saying that you wrote |
I got the principal now, and I propose a change in my --emitExtension PR at #35148 (comment) |
My summary on Node ES Modules and TypeScript incompatibilitiesDynamic modules / Named imports from CommonJSCurrently Only Why is this the case?Dynamic modules has not gained consensus within the working group. Alternative solutions have not been accepted either. TS Author compromise
Problems with compromise
Required extensionsExtensions are required, current auto-suggestions do not add extensions automatically and still report the code as OK which will definitely cause confusion. Why is this the case?Extensionless imports failed to gain consensus although discussion is still ongoing. TS Author compromiseAuthors can add extensions themselves for ES output. Problems with compromiseAuthors must take care to add another
|
I have never even considered using this features for this reason. However existing complexity doesn't mean the alternative system increases complexity. It may be the case that within roots that have a Speaking for myself, all I really want a is a tool that turns a graph like this: Into one where the Maybe I want modules: Any maybe I want CommonJS with In these graphs the specifier always points to the type of resource I want, but what I want In such a graph |
Because it hasn't been. There are just individuals with opinions - I'm one of them. |
Well some agreement must've been achieved to unflag the current implementation (even if it is still experimental), this seems like a pretty strong signal that the core implementation of modules is nearing readiness even if there's still rough edges. Also the problems will still be present even with auto detection, as TypeScript will still need to learn to understand the extensions if it wants to be able to support importing packages that use Although I do think this is one of the lesser issues compared to the others I mentioned as TypeScript authors can always decide just to publish non-mixed types for the time being. |
It is how it is because it's easier to add extension resolution back in than to remove it. That's what we could agree on. |
Just to clarify: You can't |
I have fully rewritten the pr, for any one interested, see #35148 |
Just saw that PnP had been discussed:
I'm curious if there are suggestions you have regarding our design that would make it easier to reach a solution (outside of plugins - I mean default support)? |
Apologies for discovering this issue so late, and for my limited understanding of TypeScript’s constraints regarding this issue. Back in 2016 I added support for For an intended runtime where ES modules are supported, like modern browsers, this works great; the author just needs to write browser-compatible I gather from the discussion here that lots of people prefer to have TypeScript be their only build step, so a secondary step for renaming files is undesirable. Has it been considered that the TypeScript compiler have an option to just output |
The conflict stems from TypeScript assuming that when you write TS code, the code will target a single runtime environment and you will know the file names/paths at dev time. Many people want a workflow that doesn't know file names/paths until compile time, and the introduction of NodeJS's |
In the final implementation, Node supports ES modules that use |
I haven't been following this aspect of NodeJS closely, but that statement doesn't align with the comments at the top of this issue (that mjs extension is required). If NodJS does go with |
I have tried to rewrite extension at compile time in my pr mentioned above (--emitExtension) but typescript team does not accept it. |
Use the |
I’m on the modules team for Node.js. There was an earlier experimental ES modules implementation in Node 7 through 11 that required This was added specifically to support file types such as TypeScript and CoffeeScript and JSX that have their own extensions ( What issues, if any, are there with TypeScript outputting ES module |
@GeoffreyBooth Given what you said above about latest NodeJS, it sounds like An example of where this is problematic is ts-node, which runs on NodeJS and hooks into the NodeJS module loader. Unfortunately, the NodeJS module loader doesn't callback to external module loaders if the path has a NodeJS could give external module loaders the ability to hook into JS file module loading (an opportunity to do path transformations) which would solve the immediate problem for ts-node (this may be something you care about) but it doesn't solve the broader issue of making TypeScript (the language) agnostic to runtime environment. |
So
I don’t follow. If the user can write This gets back to my earlier question. Can a user just write |
Yes. If you are targeting only browser and NodeJS this is the commonly recommended strategy. If you are targeting things besides those two (like ts-node or some hypothetical future non-JS runtime) then this strategy doesn't work (without hacks) because your emitted files may not have a |
For an example, one can imagine a runtime that runs TS natively (no transpilation to JS). If you were to do Currently, TS is agnostic to the target language/file types. At dev time you don't have to make a decision on target runtime, you can put that decision off until compile time or runtime. Requiring that users explicitly provide an extension at dev time breaks that agnosticism and pushes TS toward gnostic language (coupled with the runtime target). This breakage of agnosticism is what I dislike about the current proposed path forward on this issue. FWIW, I do appreciate that MS has made their stance on this clear, and I respect that they disagree as to the value of runtime agnosticism. |
So I don’t consider It seems to me that if the TypeScript compiler itself can see |
If you believe TypeScript should only ever compile to JavaScript and will never be executed natively (without transpilation) and will never compile to anything other than JS (e.g., WASM, native, etc.) then specifying As an example, Kotlin can compile to JVM bytecode or JavaScript. It is reasonable to write a Kotlin library that isn't runtime specific. If Kotlin required you to specify the file extension of relative imports, you would not be able to author code that worked with either runtime... it would change what is now a compile time decision (runtime target) to a dev time decision. |
@weswigham you could wrap all dynamic import paths in a helper that does the rewriting at runtime? |
Such a large generated runtime component is something we're not interested in. We've tried very hard to keep our helpers minimal we do not provide a runtime, per sey, and don't provide full polyfills ourselves. if you wanted that runtime behavior, you'd need to provide it yourself. Plus, such a helper is inherently falliable in the presence of edge case files with strange chains of extensions like "file.js.ts", when used with cjs style resolution. :( |
(My custom transformer did that) |
I think for dynamic The way to be sure that there isn't some CommonJS trickery going on like |
The edges of a system are what define its limits. You do realize that, under a scheme like that, resolving that specifier went from "it does what it does based on cjs" to "that, except for these cases, except in these scenarios" right? It's a minefield. Especially when you consider cross-project references, and references to (declaration) files that are just straight up are not in the current build. Hell, the declaration file for a file doesn't even encode the original source file's extension in any way (it could be tsx (and therefore jsx) or TS or js or jsx)- it's just associated by location. This is a minefield we will not enter. |
But this is the problem. CommonJS-style resolution is a Node-specific thing. TypeScript isn't and shouldn't be defined by Node. If it were me, I would create a new configuration option called “rewrite extensions of static imports of transpiled files” that does just that: when the transpiler encounters a static specifier of a file that the transpiler itself will be transpiling later (or has already transpiled), that specifier is rewritten. It's that straightforward, and it's not a minefield. Make it an experimental option if you want, and see what edge cases it can't cover. If too many people find too many cases where it doesn't rewrite as they'd expect, drop it. But I'm not persuaded that it's unachievable, or that the definition of when something will be rewritten is so difficult to fathom that users can't grasp it. |
We added an option called A lot of our emit happens through What is a file extension, anyway? Is it unfathomable to see a repo with filenames The current rule could not possibly be simpler, and cannot possibly break a working program. You'd have to convince us that there's a huge pot of gold on the other side of the rainbow to give that up. |
the extension is always and only the final piece ( |
(┛ಠ_ಠ)┛彡┻━┻ |
I want to repeat this because it is such a strong point:
This is the thing - a mode where you always apply a file extension change is wrong because it can't resolve. A program that resolves to determine emit on extensions is fundamentally incompatible with And in either of these modes, "what if the |
Forgive my ignorance, but how do you determine what files to transpile? I assume there's some configuration to define something like “include So basically, step 1 is to identify all the files that match that glob; and then step 2 is to transpile each of them. Since in step 1 you have the full list of all files that will be transpiled, in step 2 you can pass that list into I understand the reluctance to fix something that isn't broken, in TypeScript maintainers' view, but browsers and Deno will never support CommonJS-style implicit extensions and it's not likely that Node will either (for ESM). If that's what TypeScript continues to insist upon, it will increasingly feel like a bug to users. Maybe that's not a “pot of gold,” but I think good UX is worth striving for. |
This is what we're saying is not the case.
Again, nobody's insisting on this. You can add a |
For Node, but for IDEs? Is Code going to start seeing I understand that changing the model of |
Yes because TypeScript is literally the thing that powers the TS/JS IDE functionality in VS Code. |
And if you have TypeScript editor functionality that isn't powered by...TypeScript, well, resolving that way is how TypeScript works so you'd have a bug if it wasn't implemented that way. |
// <root>/apple.ts
export const apple = 'apple'
// <root>/index.ts
import { apple } from './apple'
console.log(apple) Am I incorrect in assuming that when import { apple } from './apple.js'
console.log(apple) If that assumption is incorrect, how does |
As a user, the scenario above is really the only time I want TS to rewrite imports for me. It is in this situation specifically that TS has more information than I do as the developer, and thus can more accurately figure out what the correct import URL should be. If I am importing something that resolves to a The problem with writing |
@RyanCavanaugh the pot of gold is the possibility to download any TS module from any place in the world via URL without any dependency like |
Having individual file emit depend on disk/internet content would be a big barrier to that goal, not something that helps it in any way. |
How @weswigham? You can cache content. |
That's still miles worse than not needing to fetch the content at all. "Just cache it" does not magically make it free, it just amoritizes the cost over many similar requests. Requiring dependency information to solve emit is just as bad as type directed emit, imo. And I can talk from experience here, as while we take the position that we don't rewrite import specifiers and always have, we do rewrite triple slash references (which are largely a legacy feature, which modules should never need to use) to referenced declaration files in declaration emit, and it is the biggest pain, and has resulted in hundreds of bug reports (many of them performance related!), and inspired features like |
I wished there was an emoticon expressing thank you so that I would not have to waste space in this thread thanking you @weswigham. So let me thank you for this great reply and the very good reasons (although my stupid intuition is still disagreeing with you) nevertheless in this way. |
So what about my initial PR content? Don't smartly try to resolve the correct file extension, add it dumbly and tell the developer the rule of adding trailing extension. Developer should ensure the correctness by themselves |
@Jack-Works I think the overall sentiment is that always applying the extension change is likely to still have its own share of surprises which we're not convinced would be worth it:
|
If specifier and file extensions are driving you bananas then try @knighted/specifier to change them however you like. By the way, support of |
.mjs
Input Files#27957
Node.js shipped module support recently
How's this work?
require
always does a CJS resolutionIdea: if what Node has today is the best that it will ever deliver...
.mjs
resolution, but error on that.import(...)
would...need to not be imported.Wait, why would you want any of this?
Should we emit
.mjs
files?.ts
files to.mjs
files because that'd be a massive breaking change.module
field?.mts
and.cts
?.mtsx
and.ctsx
?.d.mts
and.d.cts
?As long as we don't rewrite imports (and we won't because it's error prone and requires whole-program knowledge), then we need the disambiguators.
Take the following example
require("foo")
breaks because it'll resolve tofoo/index.js
import "foo"
doesn't work because it won't resolve to/index.js
because Node thinks it's too magical for ESM.This logic is extremely complex.
Conclusion:
.mjs
in an import path is doable long-term, but not.mjs
output is troublesome.--emitExtensions
and Importing.ts
Files#35148
.ts
extension to get custom file extension #30076"moduleResolution": "deno
"?https://
paths?PnP Resolution
#35206
.js
file.Reorder Extension Priorities
#34713
The text was updated successfully, but these errors were encountered: