Description
I suspect this is a feature many people would like, though perhaps not as many as were the case for Underscore. I personally think that there would be value in being able to import Backbone.Events
, Backbone.Model
, Backbone.Collection
and perhaps the extend
class emulator without importing the entirety of Backbone. Modularizing Backbone has been part of stage 2 of my maintenance takeover plan (#4244 (comment)).
@vtalas started an attempt in #4215, but seems to have abandoned the branch since. As I will discuss below, I can think of a few good reasons why he might have given up.
I decided to make my own attempt, partly because I wanted to focus on regenerating a backwards-compatible UMD bundle using Rollup first, and partly because I didn't like Node.js's idiosyncratic .mjs
extensions. I was planning to go a similar route as I have done for Underscore (see jashkenas/underscore#2826, jashkenas/underscore#2849 and jashkenas/underscore#2914), with .js
files in a modules
directory that has a nested package.json
with type: "module"
. However, I ran into serious difficulties. Unless some Rollup or ESM guru can help me out, I'm afraid we will have to face some hard choices.
Below is a link to my attempt-in-progress. The branch is not at all clean, so please do not base new work on top of it. Rollup would be invoked with npx rollup -c --bundleConfigAsCjs
.
master...jgonggrijp:modules-2
The difficulties relate to the way the present, hand-written UMD bundle treats jQuery as an optional import.
- The factory assumes that if jQuery is available, it is available synchronously. For this reason, I cannot use dynamic
import()
to approximate the behavior (unless I optionally import jQuery and then call a factory function that builds Backbone around the provided import, but that would defeat the purpose of modularization). Even if I could somehow useimport()
, it begs the question whether Rollup would translate that to a UMD bundle that is backwards-compatible with the existing bundle. - In the CommonJS case, the existing UMD wrapper wraps the
require('jquery')
call in atry
/catch
. As far as I can tell, there is no Rollup parameter or plugin that lets me replicate that behavior. - In the browser globals case, the existing UMD wrapper attempts to find jQuery under various names with the expression
root.jQuery || root.Zepto || root.ender || root.$
. Again, there seems to be no way to reproduce this logic with Rollup. I can pass such a string tooutput.globals.jquery
, but Rollup attempts to use the entire expression as a single property lookup in that case.
I have tried and considered several solutions, but none of them is (1) treeshakable, (2) backwards-compatible and (3) sourcemap-tracable to the source modules at the same time:
- I could use
@rollup/plugin-alias
to replace the jQuery import by a hand-written file that attempts to replicate the behavior in the existing UMD wrapper and thenexport { $ }
. It would dorequire('jquery')
in the AMD and CommonJS cases and theroot.jQuery || root.Zepto || ...
thing in the browser globals case. Alas, doingrequire('jquery')
in the AMD case means a breaking change; it replaces async module loading by sync module loading, which in most cases will likely not even work. I cannot dodefine(['jquery'], ...)
in the AMD case, because that would not produce the$
on time in order to export it. - I looked into writing a custom Rollup plugin in order to modify the internal logic of the UMD wrapper, but it is unclear from the documentation whether any hook will actually allow me to do that. Even if there is a way, such a patch would have to update the sourcemap, something for which I currently lack the expertise.
- I could more bluntly patch the UMD wrapper with
@rollup/plugin-replace
or a similar plugin. However, this is an extremely fragile solution that would break the sourcemap as well. Also, it is again unclear from the documentation whether I can even modify the UMD wrapper in this way. - I could make Rollup generate a bundle, trim off the header and footer entirely, and embed the remaining code in an completely hand-written UMD wrapper that looks exactly like the existing one. This would work and would be backwards-compatible, but again, there would be no sourcemap. It also feels like a rather questionable solution.
So unless somebody can point me to a golden solution, I'm afraid we will have to make at least one of three sacrifices:
- Give up on treeshakability. This basically means giving up on ES modules entirely. While this is a valid option, it does not seem very future-facing.
- Give up on backwards compatibility. Also a valid option, but I feel that now is not the right time for a 2.0 version bump, so this would likely delay modularization substantially. This option would also mean figuring out an entirely new approach to the optional jQuery dependency.
- Give up on sourcemapping from the generated UMD bundle to the source modules. At first sight, this may seem like a minor sacrifice compared to the other two, especially given that Rollup keeps the bundle very readable. However, as I described above, every approach I can think of to make this sacrifice without the other two is rather iffy.
So, what do we do? Please share all the opinions, ideas and suggestions you may have!