Move from CoffeeScript to ES6

Considering:

I consider it time that we migrate to ES6 for our modules, one by one, measuring the impact as we go.

Considerations:

  • We would essentially be dropping native support for node.js, with native support only for io.js (this could actually be a good thing)
  • We will need to use io.js with flags due to some useful features (such as classes) still being staging features

Workarounds:

  • We could build our modules with a es6 directory that contains our source files, and a es5 directory that contains downgraded compiled files (via tracuer, or es6to5, or whatever).
  • We could then either:
    • have package.json's main point to the ES5 version, with the user having to do require('the-module/es6') to get the ES6 copy
    • or have package.json's main point to a detection script that detects if we have the necessary support, and includes the ES6 version if so, otherwise defaulting to the ES5 version (this will probably cause issues with things like browserify, so the latter is preferable)

Pre-processors:

Updates:

Thoughts?

You sum up the situation pretty well.

CoffeeScript could still be relevant only if the correct decisions about its future were made. Currently, jashkenas position is stucked in a plain denial about the rise of ES6 and nothing concrete is being done for a CS version 2.0.

Some discussion on babel’s gitter here about compilation targets: https://gitter.im/babel/babel?at=550da85ba62333494b8575cc

I’ve created a CoffeeScript to ES6 comparison here: https://github.com/balupton/coffeescript-to-es6/blob/master/README.md

when reading about the Babel link you sent I was going to be like, “Ben, have you heard of 6to5…?” and then oh yeah that’s what it got renamed to. I skimmed through the log but sounds like you were able to hack it to your purposes.

So one person/group is working on a CoffeeScript ES6 compatible fork? There are already so many flavors of CoffeeScript this is hard to imagine there wouldn’t be one in the works. Is it like, that it’d be ridiculously complex for the CoffeeScript compiler to do this the way it’s written currently? (pardon I just haven’t started searching for it myself yet if it exists :mag_right: :globe_with_meridians: )

To be fair I recently saw Coffee script maintainers looking in to making some performance improvements recently.
Funny enough today I found a memory leak in Coffee script when using classes with spread operators https://github.com/jashkenas/coffeescript/issues/3236#issuecomment-85102994

I think starting on the ES6 journey now would be a good move because there is a fair amount of work to be done. I think we should keep it as minimal as possible to start with so we don’t get too far ahead of vendor native support.

Right now I think we should start with ES6 classes, arrow functions and keep on using node’s require for importing which works great in iojs with --es_staging --harmony_classes --harmony_arrow_functions). Eventually when native comes we can switch everything over to use import and unless there is a requirement to be browser compatible then all this can be done without having to transpile ES6 to ES5.

Example:

// App.js
"use strict";
let Utils = require('./utils');
class App {
  constructor() {
    new Utils().sayHello();
  }
}

// Utils.js
"use strict";
class Utils {
  sayHello () {
    console.log("hello");
  }
}
module.exports = Utils;

I moved TaskGroup to ES6 today (took 12 working hours). Changes here: https://github.com/bevry/taskgroup/compare/v4.3.1...v5.0.0 (link has since been updated for the actual final release)

@pflannery would be cool to see what the performance difference is like on your tests.

There’s a few things still do:

  1. Update documentation for all b/c breaks (renamed methods)
  2. Evaluate if maps for configuration and options and state is actually a good idea (it may not be)
  3. Evaluate if we should move the tests over to ES6 too
  4. Test it with browserify and webpack
  5. Update the base files with the new formats from the es6 branch
  6. Figure out a new documentation generator, and one that supports our new configuration and state system
  7. Replace csextends with a new babelextends

Notes:

  1. ./es6guardian.js (which is the main file) will try and use the ES6 version, if that fails, it will use the Babel compiled ES5 version instead. For now, it will always fail as V8 does not support the rest arguments which we use heavily.
  2. It doesn’t make sense building a partial harmony build that is applicable only for iojs, as node will be left behind and it is too much pain in the ass for us. Best for us to just wait for V8 to catchup. Reasoning for this is pain for us, and a lot of ES6 is still not optimised, so Babel compiled ES5 is often faster for now.
  3. The conversion to ES6 is not something that can be done automatically. The new features in ES6 require you to think about your code differently and consequently requires you to write it differently (hence all the b/c breaks). Waiting for a CoffeeScript to ES6 compiler is not going to be beneficial.

Awesome @balupton, I will have a peek when I get a chance.

Rest arguments should hopefully be landing in the next iojs too - https://github.com/iojs/io.js/pull/1232

First bit of feedback.

  • My testing is outputting a similar memory overhead.
  • Unfortunately Babel does something similar as the coffee script ctor saga (without the memory leakyness) but this time every class that uses rest parameters in its constructor ends up being called Object in v8 heap snapshots so it’s difficult to find these classes when diagnosing problems…

Luckily there is a solution to point 2 above which is to use

var instance = new (Function.prototype.bind.apply(TaskGroup, [null].concat(args)))

this is what Traceur uses to create classes that have rest parameters in their constructors. So for now I’ve just replaced the ES5 output where the rest class creation occurs.

Now that I can see Task and TaskGroup classes in the heap I will have a dig around and see why there is still so much memory usage.

Seems by 2016, TaskGroup will no longer be needed. At least for ES7 adopters.

Going back to objects from maps for options and config and state, should allow us to do things like this for setting defaults:

var o = { x: 2, y: 3 }, x = o.x, y = o.y, z = (typeof o.z != "undefined") ? o.z : 10;
var { x, y, z = 10 } = { x: 2, y: 3 };

Source: https://gist.github.com/getify/2b53198906d320abe650

Yeah destructoring is bad ass! - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

So the transition of TaskGroup to ES6 is complete (pushed but not yet published).

The CoffeeScript TaskGroup executed 50,000 tasks in 13 seconds, the ES6 TaskGroup did it in 8 seconds, with optimisations it went down to 3.5 seconds. This is in the browser with domains disabled.

Applying the updated TaskGroup to the latest DocPad, the largesite generation of 1000 jade files with a single layout went from 65 seconds with 500mb of memory, to 48 seconds with 350mb of memory. With domains disabled, it goes down to 40 seconds with 300mb of memory.

I’ll do some more reviews of this, and see what else can be done. @pflannery thanks for your efforts on this, keen to hear your thoughts too

Would be cool to see a pull request or issue on Babel for this, as it seems quite important.

I’m tempted but I find its quicker to use babel’s implementation. We’re just have to remember that when viewing the heap its worth swapping the _applyConstructor method over when using rest parameters to create classes to use:

var _applyConstructor = function(Constructor, args) { return new (Function.prototype.bind.apply(SomeClass, [null].concat(args))); }

It’s also worth taking things like this.state = {...} and enclosing it to this.state = new SomeClassState(), that way you can see how many and how much memory is being allocated to this.state, otherwise this.state gets heaped in to Object …once again though it will decrease performance but no one cares when trying to find leaks…

Thank you too for moving this over to ES6!

FYI Have made a PR to resolve class construction with rest params issue over at coffee-script https://github.com/jashkenas/coffeescript/pull/3922

Discovered something interesting today regarding the ES6 spec.

I’ve just seen from this esdiscuss topic that we wont be able to use [[call]] on ES6 classes from ES5 classes.

Example:

class SomeES6Class {...}

function ES5Class() {
    SomeES6Class.call(this); // throws an error "Class constructors cannot be invoked without 'new'"
}

I believe we should be able to replace the call with SomeES6Class.constructor.call(this).
Hopefully we wont hit this with Docpad anyway.

I’d imagine that this shouldn’t be an issue with proper ES6 support anyway, as the only time we use apply/call on a constructor is when we are simulating rest arguments new this(...args) translates to new this.apply(this, args). With proper ES6 support, the former would work without the need for apply/call.