-
-
Notifications
You must be signed in to change notification settings - Fork 5.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
ES modules exports (implements #2718) #2826
Conversation
@jashkenas I'm done writing. I would be very grateful if you could find the time to review this. My apologies for the long opening post and the big diff (despite my best attempts to keep the diff minimal)! @lohfu @mjeanroy FYI. |
Wow! 👏👏👏. Thanks for taking the time to actually make a stab at this — many people have talked a lot about it over the years, but it takes a rare soul to follow through. I'm actually starting a month of parental leave next week, so should have a good chunk of time to look at this more closely soon. Please pester me if I don't get back to you soon enough... |
@jashkenas It's a pleasure. I love this library, thank you for making it. Good luck with parenting! |
@jashkenas Any updates? |
Not yet, and we have a wedding to attend this weekend, but thanks for the
ping!
…On Fri, Mar 6, 2020, 2:45 PM Julian Gonggrijp ***@***.***> wrote:
@jashkenas <https://github.com/jashkenas> Any updates?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#2826?email_source=notifications&email_token=AAABE7HAYZNI36TQINZRIATRGEZBVA5CNFSM4K5GBDFKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEOCHDCA#issuecomment-595882376>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAABE7AN3M7EA5PYO3M7FZLRGEZBVANCNFSM4K5GBDFA>
.
|
@jashkenas Here's another ping. My apologies if this is too soon. I hope you had a nice weekend and wedding. I'd like to draw your attention to #2829 as well. If you approve of that one, I'd prefer it to be merged before this one. #2829 is a very small change. On another note, would you appreciate some help with triaging open issues and PRs? Rather than spamming your notifications by responding to each of them individually, I could create a single issue ticket with a list of PRs than can be closed, a list of issues that can be closed, a list of PRs that require review, etcetera. If you like. |
This is looking truly great. Thank you for taking the time to create such a solid draft of an ES-modularization. I really just have a few minor notes around the edges:
Bravo! 👏 |
Thank you for taking the time to review this. I'm happy to hear it looks so good to you. This weekend, I'm going to try to address your concerns as well as I can. Some comments in advance:
This is a tall order, but I agree with the sentiment in principle and I will investigate the options.
Please observe that this is by no means my intention, not even for a subsequent pull request. The current PR introduces no new syntax other than
I'm not sure about this. I'm not sure more async belongs in underscore at all, because there is already a perfectly fine library available for that purpose. I'm not sure such an introduction would require modern
Why do you want to keep everything in one file? The purpose of the module/source divide is not just avoiding repetition, although I think there is great value in DRY. It is also the only way to enable any treeshaking at all. I realize I didn't emphasize this before, but treeshaking only works when the user imports from the underscore-source, not when importing from the underscore-module. Treeshaking works when importing from the underscore-source, exactly because it doesn't contain the I'll be frank: there is no point in accepting this pull request unless you resign to the idea of splitting the source into at least two modules. However, I'm very certain that you have valid, well-thought reasons for wanting to keep everything together. I want to fully understand these reasons and I want to address them to the best of my ability. Do they perhaps include the wonderful annotated source edition that you can read through in one go? Name all of it, and I will fight dragons so we can keep as many of the advantages of a single module as possible.
Why will it? Isn't this mostly new functionality rather than a radical interface change?
As I wrote above, this wish is impossible to grant while also having treeshaking. In addition to that, if you continue to call the UMD bundle
Unfortunately, no, that file cannot be eliminated. The problem is not in the way the default export is arrived at (I could have done var _ = require('underscore'); , their var _ = require('underscore').default; instead. The only way to make the first scenario work, which I really think it should, is to offer rollup a module that has a default export and no other exports.
I do agree with this and I will do as you suggest.
No, actually I tried that first, with rollup version 1.31.1. It still insisted on creating getters for some of the exports, probably because they were aliases of other exports. The only way to get rid of the getters entirely was the I think there are three main options:
Let me begin by repeating that I believe you have good reasons for this and that I want to address those reasons as well as I can. Please do explain those reasons to me in detail. Rollup (and the hard problem of JavaScript treeshaking in general) is mainly sensitive to two things: side effects and dependency. Apart from that, the way the code is organized does not really affect its resuts. Whether one function goes before the other or the other way round does not matter, as long as neither depends on the other or writes to an object that the other might read from. Especially the question of side effects quickly becomes intractable as more common variables are involved. This is why treeshaking becomes more effective as the size of modules decreases: modules have isolated namespaces, so the finer the modularization, the less potential common state. I truly believe that further modularization, up to the point of putting every function in a separate module, would be good for underscore. I didn't do it in this PR because I believe in evolutionary change and because I wanted to keep the review as digestible as possible. But I believe it should be the next step. Apart from dramatically improving treeshaking, splitting out the source code into many small modules has a few other clear advantages:
By extension of the above, I believe that fine modularization goes before any attempt at solving circular dependencies.
Thank you! |
Using outdated rollup version 0.59.4 because the legacy option was removed in version 0.60.
Thanks for the detailed response!
👍
👍
👍 That sounds fine. I'm sorry to hear that the
That’s fine, if it's necessary. If we end up with one file as the straight ES Module exports, one as the complete mixed API, and one file as the single-object UMD export — as you have it today — that's cool with me.
Simply because it's the soul of this (now venerable) project. Underscore has always tried — and has succeeded and failed over the years, as waves of pull requests have come and gone — to be a simply-written, single-file tour through some useful functional programming idioms in JavaScript. It’s an exercise that’s supposed to be as pedagogical as it is practical. There’s a balance between performance and organizational complexity on the one hand, and readability and source code simplicity on the other. Being a short, single-file library with no dependencies is at the heart of that identity. Also, Lodash already exists. But I don't think that should be a dealbreaker — since, if we separate the If there are any other open questions you feel like I haven't addressed, please let me know. |
Great!
I know what you mean. Underscore is a very elegant library, venerable indeed. While I had read the docco rendering before, I became more intimately familiar with the code while preparing this PR and it only strengthened my opinion. It's like a good, well-paced story in which everything comes full circle at the end. With enlightening ingenuity.
I full-heartedly agree about the succinctness and the self-sufficiency. As far as I'm concerned, those can and should stay. I also see the value of the "single read", but I think there might be other ways to address that than necessarily keeping all the primary source code in one file. Maybe it is enough if we generate a monolithic, well ordered ESM using rollup and then feed that into docco. To me, it seems like that might actually be the best of both worlds. Underscore could at the same time present a holistic story and also be a prime example of how to write elegant modular code. As for Lodash. I'm a bit hurt by the remark. I don't believe that modularity is some kind of gliding scale that separates Lodash from Underscore, or even that it is the most significant difference between the libraries. As far as I'm concerned, Lodash is what it is for very different reasons. And I'm definitely not going to repeat that history. To begin with, I will not fork Underscore under any condition.
Sorry, but no, wrong. Observe that there is a factor two treeshaking difference between the underscore-source from the current PR and the experimental, fully modularized version that I didn't publish, despite both containing the exact same code with the same circular dependencies and despite me using the exact same version of rollup in both cases. As a matter of fact, it makes a great deal of difference whether the source code lives in one file or many. I can see why you don't want this to be true, ideologically speaking, but it is true nonetheless. Rollup is over its neck in the murky business of trying to statically track side effects in an impure, weakly typed dynamic language, while Underscore is a little paradise of functional, orthogonal style. Getting a factor two treeshaking improvement by changing Underscore is many orders of magnitude easier than getting that same improvement by changing Rollup. I know; I got a factor four treeshaking improvement in less than a week of editing Underscore (a factor two by splitting it in two and then another factor two by splitting it in 146), working all alone, while the Rollup team has been working on Rollup for years and they never managed such a dramatic treeshaking improvement on anything whatsoever. I'm not just talking pragmatics. There are strong, fundamental reasons for this. In 2050, ES code distributed over many files will still be more effectively treeshaken than monolithic source code. I'll bet on that. Besides that, hidden circular dependencies are uncovered by splitting everything in tiny modules and then having a tool tell you where the circularities are. That, or many hours of brain-melting static analysis by a human (assuming the human won't give up). I don't know about you, but I'm not going to repeat that effort every time we suspect there might be a circular dependency (which we might not always suspect). Given the choice, I'd rather have it modularized permanently and get an instant hint from a tool whenever somebody introduces a circularity.
Yes. I don't fully understand yet why you feel that modularity is at odds with the elegance of Underscore. So far I see no conflict, only harmony. What makes you feel different? |
This is necessary in order to preserve all the comments.
As I said, it’s an aesthetic preference that — to me — lies quite close to the heart of Underscore’s raison d'être, and is a matter of prioritization. To me, keeping Underscore a single file that can be copied, read through in a sitting, and used without any additional tooling, is very important, and a large part of the library’s original usefulness and popularity. Whether or not we support tree-shaking at all is much less important to me. (In fact, there are good arguments against encouraging it, and promoting the single, CDN-cached, 6.5k version of Underscore instead.) But let's put that aside. When you're ready with this PR, let's take a few representative functions, and you can also do them in the modularized form — and I'll take a stab at trying to satisfy Rollup’s internal logic to see if I can come close to matching them with the single-source file. And if I can't approach your results, we'll discuss it again. |
Alright. I can really relate to this. But this is not lost; the generated UMD bundle can be copied, read through in a single sitting and used without any additional tooling. Only the developer is seeing a change in this regard.
It may surprise you, but I fully agree with this. If you're using substantial portions of Underscore client side, a monolithic CDN embed is far better than a subset included in your own (probably already heavy) application bundle. Because of the caching, as you said, and also because requests to different hosts can run in parallel. I always use a mix of jsDelivr and cdnizer for this same reason. On the other hand, there definitely are valid use cases for treeshaking. I can imagine a client side application that makes heavy use of
Fine by me. I'm about to push my finished work, stay tuned. |
@jashkenas I rebased on the latest
In renaming the modules, I deviated a bit from the options we discussed before.
Please let me know whether you find this acceptable. |
No worries! I didn't take any offense — and I think that most of your game rules sound fine. I just didn't want to commit to a particular date without knowing when I would be able to look at this further. Yesterday, they announced the closure of the borders here in Chile, and we’ve been scrambling around weighing the pros and cons of cancelled flights and shelter in place orders to try to get back home. Hopefully things will quiet down after we manage to make it back. |
No problem, best of luck getting home! |
@jashkenas I'm mostly asking this out of personal concern: how are the circumstances for you and your family? |
We’re good! When they announced the impending border closure, and our Aeromexico flight was cancelled, we turned around, drove 6 hours back north to Santiago, and grabbed a flight (one of the last?) back home to go hunker down. |
@jashkenas I'd really like to move on with this PR. Could you give me a status update? |
I went back to work on Monday, and haven't had a chance to take a look at this again ... but I'll try to soon. Sorry for the slowness. |
Alright! I took a good long crack at playing around with Rollup’s treeshaking this morning, and feel like a learned a lot about what kinds of patterns it can and can’t treeshake, how For example, here’s a pattern that Rollup can tree-shake away (it’s a reduced test case of our function _toString() {}
export var isArguments = (function() {
return _toString(arguments) === '[object Arguments]' ? 0 : 1;
}()); And here’s the version we need to use, that Rollup can’t: function _toString() {}
export var isArguments = (function() {
return _toString.call(arguments) === '[object Arguments]' ? 0 : 1;
}()); The difference in this case is Flowing through functions, this means that anything that depends on Many of the other places where we call functions at the top level can't be treeshaken by Rollup, although some simpler patterns can. So, what does that mean for this PR? Ultimately, although I'd be happy to publish a version of Underscore that provides both ES Module and UMD flavors, I think that the module should be a monolithic one, given the small size of Underscore, its presence in the CDNjs cache, and the current state of treeshaking tooling. I'm still not interested in either chopping it up into tiny files, or in littering the codebase with That said, if you want to publish your fully modularized version as a different npm package, godspeed. I'd be happy to link to it from the top of the Underscore homepage. I hope that's not too disappointing. |
You surprise me! I was prepared to wait a long time before you'd even announce a deadline for the treeshaking challenge, and now you've already completed the challenge. You did a really good job at it as well.
That's really nice of you, but no, I don't want to do that because it would amount to forking Underscore. I believe the world doesn't need yet another fork of Underscore. I only want Underscore to be modular if you are on board.
If the current, semi-monolithic PR can still be merged, I'm happy enough. How about that? And I'll try again later to interest you in chopping everything into tiny modules. ;-) |
Okay, great! In that case I think that we’re almost there. At least before I can merge it — and perhaps as a general policy — we need to provide a built version of So perhaps using Husky to define a pre-commit hook that makes sure that And then prepublish can handle the As to the actual roll-out, how would you like to handle it? I think this could merit a 2.0 release, with appropriate revisions to the homepage/documentation. Would you like to be added as a collaborator on this repo to help make that happen? I think you’ve clearly earned your stripes. |
And as to the treeshaking — my experiments make me feel like (even with a single source file), it's tantalizingly close. Rollup is able to eliminate all of the obvious non-dependencies, in terms of function declarations at the top level. It's only with our use of function calls at the top level, in order to define other functions, that it has problems, and even then, only some of the time. Things like:
... because of the call to I feel like there might be a more general pattern of dynamic function export creation we could find that would allow Rollup to treat each export in isolation ... but I wasn't able to discover it, despite my best efforts. |
Hooray!
Sure, I'll look into it.
Regardless of the version number, I definitely think some additions to the documentation would be in order. I really appreciate that you want to celebrate this with a 2.0 release, but I adhere to semver quite strictly and I think this should be 1.10. All of the old interface is still there, all of the backwards compatibility is still there and the source code still looks mostly the same. Users just get the new option to treeshake with rollup. I don't want to "shock" users with a 2.0 if it isn't actually disruptive. If, in some hypothetical future, we chop everything into tiny modules, I think that might merit a 2.0 release though... ;-)
That's an honour, thank you. I definitely want to help. If making me a collaborator helps to make me help, I'm all for it. :-) As for your second comment about treeshaking: I feel your frustration! However, keep in mind that the size of |
Right,
Okay,
Done and done.
From my understanding, not so much! Most of the half of the codebase that is currently included along with treeshaken |
@jashkenas I think we're ready for the merge. I can now push the merge button by myself, but I'll give you a chance to review the new commits first. Regarding the Husky hooks, I had to explicitly invoke How to continue from here? Merge the current PR first, then start a new branch in your repo to update the documentation? |
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.
How to continue from here? Merge the current PR first, then start a new branch in your repo to update the documentation?
That sounds like a good plan.
Given your approval, I went ahead and pushed the merge button. Hope you don't mind. This is a big milestone. Thanks so much! |
Ready for review but not ready for merging; see "before merging" notes below.
Purpose
Reformatting the underscore source code to use ES6 exports, in order to facilitate treeshaking with tools such as rollup. See #2718.
Non-purposes
const
,let
,async
or arrow functions.I agree with and followed #2718 (comment) and #2718 (comment). One change at a time.
This is also one of the reasons this isn't ready for merge yet; the move to ES6 exports obviates one level of indentation, but I kept that indentation for now in order to ease review.
Update: a commit that removes the obsolete indentation was added to this PR on 2020-03-13.
What I did
underscore.js
tounderscore-source.js
, because the former will be generated from the latter as of this PR. The generatedunderscore.js
is functionally equivalent (except for some minor differences detailed below) to the formerunderscore.js
.underscore-source.js
is just a working name; see notes under "before merging".export
statements must be at module level and secondly because rollup will generate an IIFE for the UMD bundle.underscore-source.js
in the following ways:_.name = function(){};
byexport function name(){}
._.name = expression;
byexport var name = expression;
.var internalName = function(){};
byfunction internalName(){}
for consistency with 1. and also for brevity and a little added convenience (i.e., hoisting)._.name
by justname
, except for_.iteratee
and_.templateSettings
because users may override these._.
namespace for internal use introduced some naming conflicts, which I solved as follows:has
and an exportedhas
. I renamed the latter to_has
for internal use, because the non-exportedhas
is used more._has
also resembles the old_.has
.flatten
/_flatten
.isNaN
has a counterpart that is an ES builtin. I renamed the former to_isNaN
for internal use.isFinite
/_isFinite
.keys
is used very often as a local variable inside functions. I renamed the exportedkeys
to_keys
for internal use.now
is also the name of a local variable inthrottle
, which is the return value of the exportednow
. I renamed the local variable to_now
.underscore-module.js
(working name) which takes all the exports fromunderscore-source.js
and mixes them into_
. This is a trick to avoid hand-coding all the exports twice, i.e., to avoid having to write bothexport name
and_.name = name
.module
field in thepackage.json
to theunderscore-module.js
, so that tools like rollup will be able to find theexport
-based version while everyone else can continue using the UMD bundle.underscore-umd.js
(working name) that re-exports only the default export fromunderscore-module.js
. This facilicates the UMD bundling by rollup.Update 2020-03-26: in the meanwhile, the roles between the underscore-module and the underscore-umd has reversed. The latter is the module that mixes all functions into the
_
object and the import order is underscore-source -> underscore-umd -> underscore-module.legacy
option, which prevents rollup from generating getters, which would be incompatible with IE8.noConflict
function to rollup, which has an option for this.scripts
) to thepackage.json
to generate the UMD bundles (both theunderscore.js
and two bundles for the treeshaking tests) and prepended these commands to other commands that depend on their existence.eslint
, and added a plugin, to enable the linter to processimport
/export
syntax.I did not convert the tests to ES module syntax; in fact, they still import from the (now generated)
underscore.js
.Update 2020-03-26: In the meanwhile, in addition to the above, I have done a few more things which are detailed in the comments below.
Interface changes
window
embed). However, it is a bit less refined than the former hand-written version. I don't expect it to cause issues, but I should still mention that it isn't 100% equivalent.noConflict
method generated by rollup is 100% equivalent to the fomer hand-written version. However, it is only added in thewindow
embed scenario. ESM, AMD and CommonJS module users will not see the method anymore, although they shouldn't need it, either.I believe that the published UMD interface is otherwise completely identical to what is currently on the master branch.
This PR shows a comparison between the former hand-written
underscore.js
and the new reformattedunderscore-source.js
. In order to appreciate the interface changes, one might want to see a comparison between the former hand-writtenunderscore.js
and the new rollup-generatedunderscore-js
instead. I pushed a secondary branch to make this possible. The diff can be found over here: https://github.com/jashkenas/underscore/compare/master...jgonggrijp:es-module-compiled?expand=1#diff-0f36b362a0b81d6f4d4bfd8a7413c75d (you may have to click "load diff").Quantitative changes
The amount of code to maintain decreases very slightly.
underscore.js
_source
+_module
+_umd
The weight of the published gzipped + minified UMD bundle increases by 240 bytes.
In exchange, users get the option to treeshake when using tools like rollup. If a user imports only
map
(fromunderscore-source.js
), underscore's contribution to her UMD bundle decreases by 900 lines of code compared to the present situation. This is substantial, if not as spectacular as one might expect. This is due to rollup not being clever enought about side effects and erring on the side of caution.As an experiment, I also created a completely modularized version where each function lives in a separate module (not published). This enabled rollup to shave off another 460 lines of code. Rollup also surprised me by pointing out circular dependencies, which it apparently cannot do if all code is in one module. I suspect that solving these circularities might cut
map
's size in half again, but I have not tried this.map
in lines of codeWhile the minified + gzipped UMD bundle weight has increased a bit, these numbers make me believe that using ES exports is definitely a move forward. In addition, solving circular dependencies might also decrease the weight again.
Update 2020-03-26: updated treeshake performance numbers can be found at the bottom of #2826 (comment).
Before merging
underscore.js
should probably be commited. What do you think would be the right policy for this, @jashkenas? Update 2020-03-26: in the meanwhile I have proposed to never commit it and instead ensure that it is uploaded to NPM by enforcing the build in theprepare
orprepublishOnly
hook of thepackage.json
.underscore-{source,module,umd}.js
. Perhapsmodules/{index-unmixed,index,index-umd-wrapper}.js
?Edit 2020-03-7: Changed suggested names from
modules/{underscore,index,umd}.js
tomodules/{index-unmixed,index,index-umd-wrapper}.js
because the former might be problematic if we modularize the whole library in a subsequent PR.Update 2020-03-27: slightly changed the above suggestion to
modules/{index,index-all,index-default}.js
, respectively. This suggestion was also implemented in a new commit that has been added to this PR.Edit to add on 2020-06-18: You can support my work on Underscore and other open source projects on Patreon.