-
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
Synthesize namespace records on CommonJS imports if necessary #16093
Comments
This is fantastic. I want to add that one benefit of this is that it simplifies documentation, tutorials, and other learning materials that currently either have long explanations regarding these differences or, as is more often the case, only show an import usage example that works for either the TypeScript community or for the Babel community but not for both. |
This is great to hear and I'm really glad TypeScript is looking to support this interop. @DanielRosenwasser do you perhaps mean to correct Just to share the current shift SystemJS has had to make wrt interop - I know this has probably been discussed ad nauseam, but just to voice my opinion again... Namespace lifting for CommonJSNodeJS doesn't seem to have any plans of treating CommonJS modules as namespaces, and instead simply providing them as default exports, due to all the edge case concerns here of calling getters etc. While I know this is gambling on a possibility, I think it is worse to gamble that NodeJS will get over this restriction any time soon in its adoption of ES modules. SystemJS took this hit already (while using __esModule as a fallback method), and it has definitely affected users. Just thought it is worth mentioning as I do think it would benefit TypeScript to double down on the syntax Edit: and perhaps Node core modules can be treated as exceptions here due to the expectation that in the conversion to ES modules, NodeJS core would permit named exports for these. |
Just a thought, but I wonder if it might be worth adding another I've been recommending the Also, CC @unional, @Kovensky |
Thanks @aluanhaddad, for notifying me about this wonderful issue. Why can't we add more than one 👍 to the OP? 🌷 I have also been recommending the However, I want to bring up that unfortunately, it cannot be a long-term solution. The issue is browsers' eventual support of ESM. When user target This means CJS -> ESM interop and a unify view of CJS <-> ESM is still needed. And it also means magically converting CJS -> ESM (for the dependencies of the compiling package) is still needed to be performed during compilation. (edit: we should point this out to the nodejs folks as their decision on this prohibits code sharing on server and browser unless all dependencies are ESM) Another major issue needs to be dealt with is typings and direct ts source code distribution. |
@unional I think that the
@guybedford Yes, thanks, I corrected my post.
@guybedford I agree, but unfortunately, a lot of people are extremely turned off by that idea for philosophical reasons. The existence of
@aluanhaddad that's something @mhegazy and I have discussed at some point, and I think it'll be on the table, but since nobody really knows what will happen in 6 months time, I think we should wait. At the very least, adopting this behavior will mean that there is a smooth migration path for users (as opposed to now where users can't get ready for a change like that at all without an external tool). |
After our design meeting today, I don't think this is 2.4-bound, but what we're looking for is the best migration path we can conceive, along with a story that synchronizes with type declarations on DefinitelyTyped. Current concerns are that the Babel emit does not allow namespace imports to be callable, which actually does break a lot of (technically invalid) code, such as the following: import * as padLeft from "pad-left";
padLeft(10, "zup doug"); So a potential modification could be the following function __importStar(mod) {
if (mod && mod.__esModule) return mod;
// For callable/constructable things, tack properties onto a function record
// that passes arguments through to the actual import
var result = !(mod instanceof Function) ?
{} :
function() { return mod.apply(this, arguments); };
if (mod != null) {
for (var k in mod) {
if (Object.prototype.hasOwnProperty.call(mod, key)) {
result[k] = mod[k]
}
}
}
result.default = mod;
return result;
} But we might also just be willing to have a new mode that goes with Babel's emit and breaks away from the old behavior in that respect. |
The behavior of All non-star-import emits should behave as if a hidden Any of the changes to make TypeScript any closer to ESM/CJS interoperation spec compliance will be breaking because of how off the TypeScript implementation is from the actual proposed interoperation semantics. This would also need stricter semantics than |
A lot of libraries have started to do stuff like this: module.exports = BigNumber
module.exports.BigNumber = BigNumber
module.exports.default = BigNumber To support nicer ES6 imports à la import { BigNumber } from 'bignumber.js'
import BigNumber from 'bignumber.js' the majority of libraries is not using a transpiler and will have to support CommonJS/Node imports, so it would be very sad if this is broken |
As an update on #16093 (comment), note that NodeJS has an ES module implementation PR at nodejs/node#14369 with no plans to support synthetic exports at all. This implementation will likely be released early next year sometime. TypeScript has the ability to start preparing for the impending break of losing named exports for CJS here already. |
As a follow up, has it been considered before to support a destructuring variation of the Something like: import { destructuredExport } = require('pkg'); as sugar for That could complete the syntax on this form making it a syntax specific way of indicating this interop pattern. Come on TS team, bite this bullet before it bites you :) |
In the interim would it not be possible to remove the TS1202 warning
And output
as that is in fact what the user asked for? Currently it is not possible to target both es2015 and commonjs when importing a library which exports itself as a commonjs module. This is a common scenario when developing UI code which is unit tested in nodejs and bundled for the browser using webpack/rollup. In order to support nodejs it is necessary to compile to commonjs, but in order to take advantage of tree-shaking and module hoisting when bundling it is necessary to compile to es2015 modules. It is not possible to use The previous workaround of using |
@frankwallis Yes, if it was known that the user expected that to emit to CommonJS, but not if they were asking for AMD or SystemJS. |
@DanielRosenwasser thanks I see the problem now. Although I am yet to be convinced that there is a real-world use-case for mixing ES2015 imports and amd/commonjs... It seems to me that resolving ES2015/CommonJS interop could be a long drawn-out process, and in the meantime this is causing real problems. Perhaps a better interim solution could be to output |
A module written with |
@DanielRosenwasser is there anything to prevent typescript from compiling
to
when outputting ES2015 modules? [Optionally this behaviour could be controlled by the |
@frankwallis @guybedford Imo that doesn't make sense. CommonJS has a import mymod = require('mymodule').default it should become import mymod from 'mymodule' as that is only shorthand for import {default as mymod} from 'mymodule' if you do import mymod = require('mymodule') it should be become import * as mymod from 'mymodule' as it is today. |
And that is why this whole situation is a mess. It should not as (in the actual commonjs interop spec that was not written at the time TS implemented modules) the The |
First, on the behavior of our downlevel esm loader polyfill: With the linked PR and its associated flag on (to make us do same thing babel does), Secondly, neither
Assuming the context of the linked PR where we align with babel, none of the above, since |
Can you provide some code that would make the ModuleRecord for a module be Separately, this file actually ensures that it will not have an |
@ljharb this is possible to do with live bindings It may be worth React including the __esModule attribute for named export support in interop. |
Fair enough; but that's still not up to the loader - it's up to the module. |
// self.mjs
import * as namespace from './self'
export { namespace }
This looks like a highly specific corner case, though, and most certainly does not imply that
(I don't even know if this would work or if it would throw at runtime) |
The point is simply that that is not an invariant any spec asserts, nor one anyone should rely on, as it is simply untrue. And @ljharb, the line My point is that no consumer can make sane assumptions about es6 imports without also making assumptions about the underlying loader's semantics. Babel and TS have a loader polyfill with certain loose semantics people have come to expect, while browsers have a very strict loader with ties to the document model and its execution goals, while node (experimental) has its own loader, and none of these are the same right now. ES6 modules are looking like they may become the least portable modern js you can write, since there's a good chance that an import you write for one runtime won't work in another, at least not without a transpilation tool or custom loader to translate. My only goal right now is to align with babel, since it's loader polyfill is a community winner (I mean, we half supported it two years ago already) in order to remove one instance of the competing standards problem from the equation. If node also manages to get its ducks in a row and find a solution that allows it to adopt similar semantics, then everyone but browsers will at least be using the same or similar loader semantics, and hopefully we cut the number of common variations to two. Re @guybedford:
Their index require's an esm-transpiled module and sets it as the module exports object. That import will have an __esModule marker as transpiled esm usually gets one, ergo when that import is assigned as the module exports object, the module will have an __esModule marker. So they should already be good. Probably. Not sure what that conditional |
@weswigham i will concede that “Foo !== Module” isn’t a guarantee provided by the spec. However, the characteristics of the Modue Record object ( The conditional default thing is explicitly to unwrap any Babel interop if it exists, so that CJS can require the object directly. |
@ljharb the characteristics are guaranteed by v8 - Node can't override them even if it wants to. |
So then the question is, what happens now in typescript when you import the module record and the default export from ‘react’? What are the values of Module and Foo? |
@ljharb it depends if NodeJS will support __esModule (and accept CJS executes early in the pipeline) or not, as it has the ability to set the precedent here. But if React does not export __esModule, then React is only equal to |
Based on react/src/React.js#L34 and react/src/React.js#L75 That seems to be the intent of the library. |
@guybedford node will not support that; runtime determination doesn't provide the ordering guarantees that the committee intends (and if needed, the spec will be reworded after this month's meeting to ensure that this is mandated). |
@ljharb CommonJS is a separate parse goal whose execution semantics are not defined by the specification for interop. There is a TC39 agenda item to discuss this at the coming meeting - it is not black and white by any means. You will get a chance to discuss this further at the end of the month, and I hope you will think a little more about what your stance is here. I personally believe this sort of a tradeoff would be a worthy one in the name of the health of the ecosystem. |
@guybedford the discussion we had at TC39 around CJS was effectively that ordering must be maintained; as far as I'm aware, the situation with what node will do with CJS, and thus what TypeScript should do, has not changed (and I believe, will not). |
@ljharb yup agreed this is the future now. I hope this gives TypeScript the confirmation it needs now to properly fix default exports in CommonJS interop. |
Just a heads up - I will likely be opening a new issue in the coming months to discuss native NodeJS default export form interop support in TypeScript. If there's an issue already tracking this please let me know too. |
@guybedford It'd just be the new |
@weswigham exactly I guess it is to use synthetic defaults on CommonJS modules and namespaces on ES modules, where the format for which way to go on this is determined by the NodeJS resolver. |
Although synthetic defaults still seem to work side-by-side with namespace interpretations. It would have to be a form of synthetic that disallowed non-default named exports entirely. |
@guybedford for my own reference for such a future flag, I should note that |
Sure thanks @weswigham for noting it. It's good to prepare for this to dampen the landing as modules hit platform reality. |
TL;DR
Flags like--allowSyntheticDefaultImports
will just work without extra tools like Webpack, Babel, or SystemJS.TypeScript will just work with the
--ESModuleInterop
flag without extra tools like Webpack, Babel, or SystemJS.See the PR at #19675 for more details.
Background
TypeScript has successfully delivered ES modules for quite some time now. Unfortunately, the implementation of ES/CommonJS interop that Babel and TypeScript delivered differs in ways that make migration difficult between the two.
TypeScript treats a namespace import (i.e.
import * as foo from "foo"
) as equivalent toconst foo = require("foo")
. Things are simple here, but they don't work out if the primary object being imported is a primitive or a value with call/construct signatures.ECMAScript basically says a namespace record is a plain object.
Babel first requires in the module, and checks for a property named
__esModule
. If__esModule
is set totrue
, then the behavior is the same as that of TypeScript, but otherwise, it synthesizes a namespace record where:require
'd module and made available as named imports.require
'd module is made available as adefault
import.This looks something like the following transform:
As an optimization, the following are performed:
If only named properties are ever used on an import, *the
require
'd object is used in place of creating a namespace record.Note that this is already TypeScript's behavior!
If
__esModule
flag is not set on therequire
'd value, thenA fresh namespace record whose
default
export refers to therequire
'd value will be created in place of therequire
'd value.In other words:
Note that there is no optimization for never using the default. This is probably because you'd need alias analysis and could never be sure that someone else would use the
default
.Proposal
I believe that we'd serve both communities well if we decided to adopt the aforementioned emit.
Drawbacks
Performance & Size Impact
This certainly impacts the size and readability of TypeScript's output.
Furthermore, for large CommonJS modules, there could be a speed impact to keep in mind - notice that these synthesized namespace records are not cached at all.
Tools already do this for us
Those who've been using tools like Webpack 2 and SystemJS have been getting this functionality for a few months now.
For example, if you target ES modules, Webpack 2 will already perform this behavior.
Taking this strategy makes it an opt-in for users who care about the interop behavior.
I mainly bring this up because I want to immediately bring up the counterpoint.
While these tools are definitely central to many modern web stacks, they won't cover
The text was updated successfully, but these errors were encountered: