Bear with me, this PR is a bit of an epic...
Following the discussion on https://github.com/mrdoob/three.js/issues/4776, this PR converts the Three.js codebase to ES2015 modules, and updates the build process to use Rollup (full disclosure – I'm the author of Rollup).
The resulting build/three.js
file is functionally identical to the existing one – the examples continue to work* without modification – albeit slightly smaller when minified, because the various modules can now refer to e.g. Mesh
instead of THREE.Mesh
, meaning everything gets mangled properly by UglifyJS.
Apart from the many advantages discussed in https://github.com/mrdoob/three.js/issues/4776 (easier development, better linting, simpler build process, etc), a great thing about using ES modules is that people will be able to do this sort of thing in their apps...
import * as THREE from 'three';
const mesh = new THREE.Mesh();
// ...
...and rather than including the entire library in the resulting bundle, ES-module aware tools that can do tree-shaking will be able to discard unused parts of the library.
Changes
The nice thing about using modules is that you no longer have to specify the order in which files should be included. The frustrating thing about modules is that you lose control over the order in which files are included.
Specifically, when you have cyclical dependencies – which Three.js has a few of – you're at the mercy of the topological sorting algorithm (per the ECMAScript spec). If you have a file like KeyframeTrack
which depends on the various subclasses in animation/tracks
, each of which depend on KeyframeTrack
, you end up with a situation in which the prototypes of the subclasses don't inherit from the parent because the execution order is all wrong. The solution I've used here is to put the KeyframeTrack
prototype and constructor function in separate files, which allows for a stable sort. A similar thing was necessary with Shape
and Path
.
I also moved all the constants like THREE.CullFaceNone
into a separate constants.js
module so that other files in the codebase can refer to them without introducing a cyclical dependency.
Similarly, the various cases of object instanceof THREE.Mesh
and so on have been replaced with object && object.isMesh
(and the relevant constructors have been augmented such that this.isMesh
is true
for all instances of Mesh
. This also eliminates some nasty cyclical dependencies.
Finally, THREE.AudioContext
is a bit tricky because it's a getter: with modules, THREE
is just a namespace rather than an object, and while an object can have getters a namespace can't. So internally, AudioListener
etc do this.context = getAudioContext()
rather than this.context = AudioContext
. For the UMD build, the getter is added to the export, so as far as consumers of the library are concerned THREE.AudioContext
will continue to behave exactly as before.
Next steps
If you merge this (and I hope you do, but will understand if it's a bit of a leap and needs some work first!), it will pave the way for further optimisations. For example, there are lots of places where IIFEs are used to avoid polluting the global namespace – with modules, that's not necessary (everything is local), so it's possible to get rid of those. I didn't do any of that stuff with this PR because most of the changes were generated by a script (in https://github.com/rollup/three-jsnext).
Eventually this should make it easier to move certain parts out of the core library and into separate plugin repos, if that's your intention.
Let me know what you think, and thanks for reading this far!
*actually that's not quite true – the mirror / nodes example is giving me a 'Shader couldn't compile' error... I wasn't quite able to figure out what's going on, but maybe it's obvious to someone else?