-
Notifications
You must be signed in to change notification settings - Fork 134
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: allow autoloading of non-default exports #115
feat: allow autoloading of non-default exports #115
Conversation
I'm also trying to not use default exports, but I personally prefer the default export over having to import a decorator or—in this case—a Symbol. But as you've noticed, the I'll take a look. 😄 |
src/__tests__/load-modules.test.ts
Outdated
|
||
const modules: any = { | ||
'nope.js': undefined, | ||
'standard.js': jest.fn(() => 42), | ||
'default.js': { default: jest.fn(() => 1337) }, | ||
'someClass.js': SomeClass | ||
'someClass.js': SomeClass, | ||
'someNonDefaultClass.js': { SomeNonDefaultClass }, |
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.
What should happen if there are multiple class exports in a file? Which one wins?
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.
I can see two possibilities:
- the first one wins (current logic)
- an error is thrown
I thought about that while writing the code and figured letting the first one win would be the simplest. Now that you got me thinking about it some more, I'd say throwing an error is the safest course, simply because it won't create hard-to-debug cases where someone's service is not being loaded without any notice as to why.
Of course, we'll have to loop through all the exports instead of just looping until we find a valid one, but files are only loaded once in the vast majority of all scenarios.
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.
Also, it might seem weird if you have a default export and also one marked with the symbol, which would win? Not saying anyone would have a reason to do this, but its an interesting question.
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.
In that case, the default one wins because it's the first one being checked. Again, we could check for all cases, and throw an error.
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 might seem like an odd limitation. But named exports always have a name, so perhaps we should use the name of the class/function instead of the file name for non-defaults, but you then risk registering way more than you bargained for, which was another reason I was against doing this.
Thoughts?
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.
Good point. In general, I'd say promoting accepted good practice should be the general guide here, e.g. one class/major function per file. In this case, limiting to one service per file.
Although, as I said above, I don't think it matters too much how much you allow as long as errors are thrown fast and hard. Determine the name like that:
- default export -> use file name
- named export having [RESOLVER] property
- use name from inline resolver options
- otherwise use constructor name
- otherwise use file name
At every step, check if a service with that name already exists, and, if so, throw. As far as I can see, that should leave no room for unexpected overwriting.
Registering more than you bargained for should be hard if the [RESOLVER] property really is required for an export to be loaded, since it would have to be explicitly set on the export. Under the above logic, name collisions would not go unnoticed.
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.
Right, I forgot about needing the symbol.
On one hand I am reluctant about this because I’m trying to avoid API bloat, but I also don’t want to be too opinionated. 🤔
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.
So I think the logic would be
default
-> file name passed throughformatName
- named with
[RESOLVER]
symbol prop -> the name passed throughformatName
Then we don't have to throw any error either. I think that makes sense? That way this is definitely not going to be a breaking change.
To be honest I was never really a fan of allowing customization using the RESOLVER
symbol, I never do it personally, but it was requested in #34 and OP wasn't the first to request it. I really dislike having such a large API (many ways of doing things) but I also don't want to prevent people from being productive either.
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.
Sorry for the delay, am now back from Christmas vacation.
Yes, I understand. It's certainly not vital functionality, it's just irritating when one follows best practices from Google (gts
) and then gets red tslint
errors on a killer third party library.
If it helps your decision, I was inspired by this: https://github.com/Vincit/objection.js/blob/84b91aead05d834f3e6df9bbead2734090641520/lib/utils/resolveModel.js#L56
For what it's worth, I think "API bloat" would begin once you started e.g. accepting multiple services per file. Then you have to extend the logic to check they all have an explicit name, which means more checks, etc.
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.
Then you have to extend the logic to check they all have an explicit name, which means more checks, etc.
Well, I think it would actually be simpler to just register every named export that defines the RESOLVER symbol, that way we don't have to throw an error if we just use the export name rather than the file name for those (file name would be for default export only)
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.
So for some reason GH Mobile does not submit reviews. I wrote this in December.
src/__tests__/load-modules.test.ts
Outdated
|
||
const modules: any = { | ||
'nope.js': undefined, | ||
'standard.js': jest.fn(() => 42), | ||
'default.js': { default: jest.fn(() => 1337) }, | ||
'someClass.js': SomeClass | ||
'someClass.js': SomeClass, | ||
'someNonDefaultClass.js': { SomeNonDefaultClass }, |
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.
So I think the logic would be
default
-> file name passed throughformatName
- named with
[RESOLVER]
symbol prop -> the name passed throughformatName
Then we don't have to throw any error either. I think that makes sense? That way this is definitely not going to be a breaking change.
To be honest I was never really a fan of allowing customization using the RESOLVER
symbol, I never do it personally, but it was requested in #34 and OP wasn't the first to request it. I really dislike having such a large API (many ways of doing things) but I also don't want to prevent people from being productive either.
Hey @whefter , did you see my latest comments? 😄 |
Hi, saw them, was unfortunately delayed in my answering. I'll be re-working the commit this week to include every export with a RESOLVER symbol. Personally I use it all the time - it makes more explicit how the service is handled (singleton/scoped, classic mode injection, ...). Especially where the injection mode is concerned, it makes more sense to me to set that option right there in the file in which I'm writing the constructor. |
No worries! Yeah that's fair enough. 😄 Do you agree that using the export name for named exports and file name for default export is the way to go? |
Yeah. Both could be overwritten by specifying the name option in the RESOLVER symbol, meaning we need to check for duplicates and at least throw an error in that case. Am I missing something? |
The current behavior for duplicates is simply overwriting, so I think being consistent there is the best approach? |
That should be it. I split the one main test into multiple ones, each testing one aspect. As a bonus, the test is now back in its original form, so that should assure backwards compatibility. Let me know what you think. |
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.
This looks great, excellent tests as well!
Perfect, if you could squash this down to 1 commit then I'll merge! 😄 |
Actually nevermind, I'll do a squash + merge here. 😄 |
Shipped in 4.1.0, thanks @whefter ! |
Thanks for merging, glad to contribute! Yes, of course I forgot to squash. There's always something... |
Module default exports are discouraged in e.g.
tslint
; theno-default-export
setting is set to true when using out-of-the-box settings for the quite populargts
styles.This proposed PR allows for a non-default export to be loaded if it contains a static property with the
RESOLVER
key, even if that property is just an empty object. The dropped default export requirement would allow for better compliance with thesetslint
best practices.Because the default export is tried first and non-default exports must have the
RESOLVER
key, the change should be fully backward-compatible.Tests have been amended to cover this case. Suggestions are welcome!