You need a wrapper to make it work, but yes, you can do this. The first thing you need is a wrapper to make a Node-style call into a Promise:

function node(fn, ...args) {
return new Promise((accept, reject) => {
args.push((err, ...vals) => err? reject(err) : accept.apply(this, vals));
fn.apply(this, args);
});
}

What does this do? The argument of `new Promise` is a function which takes two functions, `accept` and `reject`, as its arguments. The function will be immediately run and is meant to do some stuff, possibly asynchronously, and then eventually call the first one of these on success or the second on failure. (One thing that takes a while to get used to about Promises is that generally the asynchronous thing starts when the Promise is created, so execution is not deferred ever.) We just take the args that it was called with, and append our own usual-Node-style callback to those arguments, asking it to pipe the Node error to the reject() function or the successful values to the accept() function.

Then you need a sort of "trampoline" to convert a generator-yielding-promises into a single promise. That looks like this:

function go(gen_maker) {
var gen, goNext;
gen = gen_maker();
goNext = val => {
var item = gen.next(val);
return item.done? item.value : item.value.then(goNext);
};
return Promise.resolve(undefined).then(goNext);
}

Now what does this do? It defines a semirecursive function goNext which will hand itself to any new promises. The goNext function just runs the generator for one step with its argument, and looks at what it got out of that generator. If the generator has completed, it says "I'm done, great, return that last value!" and if it hasn't completed it says "okay, I have a Promise here, I need to tell it that when it's done computing what it's computing, it should goNext() with that value, to hand that value and control back to the generator." Finally after defining this magical function we begin the first gen.next(undefined) run by creating a Promise which immediately resolves to undefined, and telling it to goNext() with it. Of course, `gen` is the output `GeneratorFunction` that is generated by running a function* generator-maker on some arguments.

With those two basics, you do eventually de-nest everything to just one layer of asynchronicity. You of course cannot do better than this to get truly synchronous code, but you might not need to if your last expression would have been undefined anyway.

For example, here is how you then write something which accepts two arguments and appends the first to the second, using a 10MB buffer in memory. (Yes, I know this is just `fs.createReadStream(process.argv[1]).pipe(fs.createWriteStream(process.argv[2], {flags: 'a'})`, with some messing around with the high-water mark; it's just to prove a point.)

// In this comment, we pretend node and go are defined as above, that we have
// checked for exactly two args on process.argv and logged some usage
// instructions if not, etc.
var fs = require('fs');
const buff_size = 10 * 1024 * 1024;
go(function* () {
var in_file, out_file, buff, count;
buff = new Buffer(buff_size);
in_file = yield node(fs.openFile, process.argv[1], 'r');
do {
out_file = yield node(fs.openFile, process.argv[2], 'a');
count = yield node(fs.read, in_file, buff, 0, buff_size, null);
yield node(fs.write, out_file, buff, 0, count);
} while (count === buff_size);
yield node(fs.close, in_file);
yield node(fs.close, out_file);
});

In terms of larger libraries that do the same, so that you don't have to roll your own `node` and `go` functions, see e.g. http://taskjs.org/ and some other similar projects.