-
-
Notifications
You must be signed in to change notification settings - Fork 770
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
feat: further es module detection #2319
Conversation
Typescript 3.9 and above generate a different type of ES Module, which we don't currently provide an error for; the mocks just silently fail. Detect this type of module, and explicitly throw an explanation here, instead of failing silently.
Hi, thanks for this. Are you sure the resulting bundle from typescript really is an ES Module or just something that has been transpiled to ES5 and made to mostly act like one? I would like to see an example project that this applies to, so that it's possible to look at the code under test after transpilation. The added code does not really give me confidence we are actually talking about ESM. In which case, the test before it, |
..including generated code?
I had left this in draft because I didn't, and still don't, really know how to write a reasonable test for it. I've pushed a test which includes some typescript source code, a The "silently fails to stub" test shows the problem, and passes (i.e. demonstrates the bug exists) without any of my other code changes. This breaks As to whether these things are es modules or not, I do not know. The Typescript team generally deviate from the spec, except for where it suits them, and they have: It's certainly not something I've ever had to care about, as a node/commonjs/JS/TS/babel/webpack user, except for when my tests start hanging without any feedback. |
The problem isn't really that it's a getter, or that it's a module. It that it's an intermediate reference, right? If you mock the real module it's fine, for which I've added an extra test. And, this is what typescript users would need to do, and I want to enforce across codebases where I'm trying to get people to use sinon and typescript together. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's certainly not something I've ever had to care about, as a node/commonjs/JS/TS/babel/webpack user, except for when my tests start hanging without any feedback.
Generally, Sinon is not concerned with module loading in any form (CommonJS, UMD, RequireJS, webpack, whatever), leaving that to dedicated products such as proxyquire and similar, as the process is different for every project and in the case of module bundlers, like WebPack, different between each major version, even. That lets us avoid a whole bunch of issues and maintenance hell. We leave that to the users who create and maintain their own stack to figure out.
From a little bit of (quite shallow) research, I found that the __esModule
field is a convention used by Babel (which probably does your transpilation) to hint to the bundler/module loader what the default export is. It is not an ECMAScript Module (which is a runtime feature), but a synthetic approximation of its behavior that works in non-supporting environments (like Node). So if we leave the world of TypeScript, Babel and ES Module for a bit, let's just look at what we know:
- we have an object whose properties (the "exports") are_ implemented as getters/setters, not values
- if you try to stub that using Sinon's normal 'stub' mechanism you get a silent error
- silent errors are annoying
Getters and setters are not a problem per se for Sinon. They can be both spied and stubbed. Of course, for that to happen, the property needs to be configurable
and no one has used Object.freeze()
or similar. configurable
is false by default in all your code examples, so none of them are "stubbable" in that sense.
So the gist of all this code is some way of notifying the user explicitly that this code is not stubbable, am I right? We have tried to avoid logging stuff like this in Sinon for the most part, so I am not totally confident about what to do here. In any case, the code mentioning TypeScript and ES Module's are a bit superflous, as they are just specific instances of the more general problem, AFAIK, and can be dropped.
if (!object || !object.__esModule) { | ||
return; | ||
} | ||
|
||
var own = Object.getOwnPropertyDescriptor(object, property); | ||
if (!own.get || own.set) { | ||
return; | ||
} | ||
|
||
var msg = | ||
'Property "' + | ||
property + | ||
'" cannot be overriden, it appears to be a ES Module read-only property. ' + | ||
"These can be generated with re-exports in Typescript."; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of all of this, I would just drop the esModule thing, check if the prop had a getter and setter (like you do) and if these were configurable. If no on the latter, we could go into the switch block. Otherwise, if for some reason Babel in the future supported exporting configurable props you could still modify the props using Sinon's getter/setter functionality.
P.S. I still think the right approach here is to write a Babel plugin that one can configure to export objects with plain props for the sake of stubbing, possibly based upon something like plugin-proposal-export-default-from. That would enable you to just add that plugin to your custom Babel config that extends the normal one and have a simplified test setup. |
Hi, again. Maybe what I mentioned above already exists in the form of the I was browsing the source for babel-plugin-transform-modules-commonjs and came across this, which is what creates the property definitions. Passing in Well worth a shot, IMHO, if it helps keep Sinon slim and lets you avoid writing a Babel plugin :) You can probably just create a test config {
"extends": "../babel.config.json",
"// your overrides here": true
["transform-es2015-classes", {"loose": true}],
} (Can't help with the actual config, alas, but the general concept would be something like it) |
There's no babel in my stack, these files are what the typescript compiler outputs. The committed example JS is compiled directly from the committed example TS with no babel. My comments above were about how the typescript compiler seems to have picked some convention (which we're calling "the babel convention"), yes. I have another (although maybe much worse) babel solution ,where I just delete the getters, but I'm hoping not to need it long term, by fixing all my tests. I'm using this patch to diagnose tests, and have raised it here so others can benefit. |
Thank you for your work. As mentioned, I think it would be more useful if not tied down to ESM being transpiled by bundlers and attack the general issue of "un-stubbability". I summed it up in a new issue: #2377 Closing this for now, but some of the code has a high chance of being reused in some form to fix #2377. Feel free to re-submit it in a form that would fix #2377 as that would help in many other cases, as well! |
Typescript 3.9 and above generate a different type of ES Module,
which we don't currently provide an error for; the mocks just
silently fail. Detect this type of module, and explicitly throw
an explanation here, instead of failing silently.
Purpose (TL;DR)
Throw an error for new types of ES Modules. We already try and do this, but it doesn't pick up typescript's ES modules.
Background (Problem in detail)
Here's the typescript team breaking us. microsoft/TypeScript#38568
How to verify - mandatory
npm install
Checklist for author
npm run lint
passes