Skip to content

ES Modules: please discussΒ #4274

Open
Open
@jgonggrijp

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 use import(), 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 a try/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 to output.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 then export { $ }. It would do require('jquery') in the AMD and CommonJS cases and the root.jQuery || root.Zepto || ... thing in the browser globals case. Alas, doing require('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 do define(['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:

  1. 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.
  2. 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.
  3. 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!

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions