import
and export
{ github, twitter, instagram, facebook }.com/benjamn
export * from
"http://benjamn.github.io/empirenode-2015"
eval
The mere presence of eval
in a
function thwarts almost any kind of optimization or static
analysis.
Most JavaScript programmers I know
consider eval
more harmful
than goto
.
Heaven forbid a snippet of user input should end up in one of those strings!
There's no good way for transpilers to
support eval
, so they
basically don't even try.
eval
when there is any other way
to solve the problem.
eval
all the time.
Of course it hides behind many different abstractions:
<script src="http://..."></script>
<script>...</script>
new Function("...")()
require("vm").runInThisContext("...")
<a href="javascript:...">Click me!</a>
<input type="button" value="No, me!"
onclick="..." />
setTimeout("...", 1000)
eval
.
Carl Sandburg
eval
in everything.Leonard Cohen
eval
But it can be tamed, and Node does this better than any other JavaScript platform, thanks to CommonJS.
require
it.
exports
remain distinct.
Once your code starts running, you get to make all the decisions about what additional code is allowed to run, and when.
From the perspective of Node and NPM,
it certainly seems so.
But what about code running in browsers?
Is "winning" really all we care about?
Can we do better?
Node has it easy. Just ask the file system!
Not so simple or efficient to do hundreds of synchronous HTTP requests over the network.
Instead, there has to be a way to deliver bundles of code to the client.*
* Bundling woes deserve a talk of their own.
Listen to
Trek Glowacki.
// wrap.js
var log = require("./log").log;
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// wrap.js
var log = require("./log").log;
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
What's wrong here?
// wrap.js
var log = require("./log").log;
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Can we fix it?
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Can we fix it?
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Can we fix it?
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Can we fix it?
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Can we fix it?
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Why does this work?
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Will it always work?
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Will it always work?
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Will it always work?
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Will it always work?
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var log = require("./log");
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Will it always work?
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var log = require("./log");
log("STARTING SERVER");
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
Will it always work?
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var log = require("./log");
log("STARTING SERVER");
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
What about now?
// wrap.js
exports.deferred = function (fn) {
return function wrapper() {
setTimeout(fn, 0);
};
};
var log = require("./log").log;
exports.logged = function (fn) {
return function wrapper(...args) {
log("calling the function");
return fn.apply(this, args);
};
};
// log.js
var wrap = require("./wrap");
exports.log = wrap.deferred(
function (...messages) {...}
);
// main.js
var log = require("./log");
log("STARTING SERVER");
var wrap = require("./wrap");
require("http").createServer(
wrap.logged((req, res) => {...})
).listen(8080);
What happens when the first request comes in?
exports
vs.
module.exports
Immediately returning a partially populated
exports
object in case of
circular dependencies would work so much better if
that exports
object was
guaranteed to become complete eventually.
But modules can change the very identity of
exports
by reassigning
the module.exports
property,
rendering that partial
exports
object totally
irrelevant.
Different modules that require
the same module
can end up with totally unrelated results!
It's definitely nice
that exports
objects can have
multiple properties.
But it's nearly impossible to determine whether a certain property is actually used.
Not a huge problem on the server, but client bundlers like Browserify and Webpack end up including tons of dead code.
module.exports
,
unless you are certain your module
has no (circular) dependencies.
exports
objects:
var a = require("a"); // Mostly safe.
var foo = require("a").foo; // Dangerous!
exports.good = function (arg) {
return a.foo("good", arg); // Uses the latest value of a.foo.
};
exports.bad = function (arg) {
return foo("bad", arg); // Uses a stale value.
};
There's nothing about CommonJS that helps you (or your teammates) keep this discipline.
But it's a shame that we're even talking about one module system winning a popularity contest.
Almost every other language avoids this contest entirely, by providing a built-in module system, and most of them do it with a special syntax.
<blink>
We have more interesting problems to solve!</blink>
import
and export
statements are so
vitally important.
The Go module system simply forbids circular dependencies.
That's one solution.
If you don't like it, don't use Go.
Thanks to the hard work and cleverness of people like Dave Herman, ES2015 modules solve or at least mitigate all four of the problems I mentioned earlier.
At first glance, the
ES2015 import
statement looks
pretty similar to a destructuring variable declaration:
let {readFile, writeFile} = require("fs");
At first glance, the
ES2015 import
statement looks
pretty similar to a destructuring variable declaration:
let {readFile, writeFile} = require("fs");
import {readFile, writeFile} from "fs";
What's the difference?
At first glance, the
ES2015 import
statement looks
pretty similar to a destructuring variable declaration:
let {readFile, writeFile} = require("fs");
import {readFile, writeFile} from "fs";
export function ensureTrailingNewline(path, callback) {
readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
Consider this usage example.
At first glance, the
ES2015 import
statement looks
pretty similar to a destructuring variable declaration:
let {readFile, writeFile} = require("fs");
export function ensureTrailingNewline(path, callback) {
readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
What happens with the destructuring version?
At first glance, the
ES2015 import
statement looks
pretty similar to a destructuring variable declaration:
const _fs = require("fs");
let readFile = _fs.readFile, writeFile = _fs.writeFile;
export function ensureTrailingNewline(path, callback) {
readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
Imported properties are simply stored in variables.
At first glance, the
ES2015 import
statement looks
pretty similar to a destructuring variable declaration:
const _fs = require("fs");
let readFile = _fs.readFile, writeFile = _fs.writeFile;
export function ensureTrailingNewline(path, callback) {
readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
This has all the problems as our earlier example!
At first glance, the
ES2015 import
statement looks
pretty similar to a destructuring variable declaration:
// Remember this hazard?
let foo = require("a").foo;
export function ensureTrailingNewline(path, callback) {
readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
This has all the problems as our earlier example!
At first glance, the
ES2015 import
statement looks
pretty similar to a destructuring variable declaration:
import {readFile, writeFile} from "fs";
export function ensureTrailingNewline(path, callback) {
readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
How about the ES2015 version?
At first glance, the
ES2015 import
statement looks
pretty similar to a destructuring variable declaration:
const _fs = require("fs");
export function ensureTrailingNewline(path, callback) {
readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
Behind the scenes, a reference to the module is imported.
At first glance, the
ES2015 import
statement looks
pretty similar to a destructuring variable declaration:
const _fs = require("fs");
export function ensureTrailingNewline(path, callback) {
_fs.readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else _fs.writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
References to the imports then get "rewritten" as if they were member expressions.
At first glance, the
ES2015 import
statement looks
pretty similar to a destructuring variable declaration:
const _fs = require("fs");
export function ensureTrailingNewline(path, callback) {
_fs.readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else _fs.writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
With ES2015, ensureTrailingNewline
always uses the latest version of readFile
and writeFile
.
At first glance, the
ES2015 import
statement looks
pretty similar to a destructuring variable declaration:
const _fs = require("fs");
export function ensureTrailingNewline(path, callback) {
_fs.readFile(path, "utf8", (err, text) => {
if (err) callback(err);
else _fs.writeFile(path, text.replace(/\n*$/, "\n"), callback);
});
}
Popular
package where this matters:
graceful-fs
module.exports
?
ES2015 modules can define
a default
exported value:
// In abc.js:
export default function a() {...}
module.exports
?
ES2015 modules can define
a default
exported value:
// In abc.js:
export default function a() {...}
// In d.js:
import a from "./abc";
module.exports
?
ES2015 modules can define
a default
exported value:
// In abc.js:
export default function () {...}
// In d.js:
import a from "./abc";
module.exports
?
ES2015 modules can define
a default
exported value:
// In abc.js:
export default function () {...}
// In d.js:
import a from "./abc";
import a1 from "./abc";
module.exports
?
ES2015 modules can define
a default
exported value:
// In abc.js:
export default function () {...}
export function b() {...}
// In d.js:
import a from "./abc";
import a1 from "./abc";
module.exports
?
ES2015 modules can define
a default
exported value:
// In abc.js:
export default function () {...}
export function b() {...}
export const c = 299792458;
// In d.js:
import a from "./abc";
import a1 from "./abc";
module.exports
?
ES2015 modules can define
a default
exported value:
// In abc.js:
export default function () {...}
export function b() {...}
export const c = 299792458;
// In d.js:
import a from "./abc";
import a1 from "./abc";
import {b, c} from "./abc";
module.exports
?
ES2015 modules can define
a default
exported value:
// In abc.js:
export default function () {...}
export function b() {...}
export const c = 299792458;
// In d.js:
import a from "./abc";
import a1 from "./abc";
import {b, c} from "./abc";
import {b as bee, c as lightSpeed} from "./abc";
module.exports
?
ES2015 modules can define
a default
exported value:
// In abc.js:
export default function () {...}
export function b() {...}
export const c = 299792458;
// In d.js:
import a from "./abc";
import a1 from "./abc";
import {b, c} from "./abc";
import {b as bee, c as lightSpeed} from "./abc";
import {default as a2} from "./abc";
module.exports
?
ES2015 modules can define
a default
exported value:
// In abc.js:
export default function () {...}
export function b() {...}
export const c = 299792458;
// In d.js:
import a from "./abc";
import a1 from "./abc";
import {b, c} from "./abc";
import {b as bee, c as lightSpeed} from "./abc";
import {default as a2} from "./abc";
import {default as a3, b, c} from "./abc";
module.exports
?
ES2015 modules can define
a default
exported value:
// In abc.js:
export default function () {...}
export function b() {...}
export const c = 299792458;
// In d.js:
import a from "./abc";
import a1 from "./abc";
import {b, c} from "./abc";
import {b as bee, c as lightSpeed} from "./abc";
import {default as a2} from "./abc";
import {default as a3, b, c} from "./abc";
import a4, {b, c} from "./abc";
Default and named exports, together at last!
And that makes all the difference.
ES2015 export
statements must
appear only at the top level of a
module, and must have one of the
following forms—all of
which have names:
export var a = ...;
export let b = ...;
export const c = ...;
export function d() {...}
export function* e() {...}
export class F {...}
export default expression;
export {a, b, c, d, e as genFn, F};
ES2015 import
statements
provide no way to obtain the exports
object
itself.
In fact, native implementations of ES2015 modules need not use objects to represent exports at all!
That's just an implementation detail that happens to be convenient if you're compiling for an environment that supports CommonJS.
This makes it possible to determine, statically,
for any form of import
statement, exactly which exports it does and doesn't care
about.
import
and export
syntax.
A tool from the future.
I would love to tell you, "Just use Meteor!" And that will soon be the case.
Until Meteor 1.3 is released, I've put together a skeleton NPM package that you can clone and modify, or just use as inspiration:
git clone https://github.com/benjamn/jsnext-skeleton.git
cd jsnext-skeleton
npm install
npm test
Try it, break it, republish it as your own, submit issues!
Some stuff you can do with this skeleton package:
src/
directory,
src/
into lib/
using an npm
prepublish command,
mocha
tests against code from both src/
and lib/
, ensuring identical output, and
lib/
and src/
code to
NPM, so everything Just Works™ but also ES2015-aware
tools like Rollup can work their magic.
.js
file they can
load with a
<script>
tag on a web
page?
Some say: bundles need to be created at publish time, so that library authors can deal with bundling errors, instead of burdening the consumer.
Others say: if library authors bundle in their dependencies, consumers of multiple libraries may end up with conflicting copies of those dependencies.
Still others: if we could all just use the same language for writing modules, then our bundling tools would have a much easier job.
Meteor's build system takes care of pretty much every aspect of bundling for you. Only binary dependencies need to be published for multiple architectures.
Meteor uses an optimizing constraint solver (compiled from C++ to JS using Emscripten) to ensure compatible package versions.
Meteor will support ES2015 modules in version 1.3, if I have anything to do with it. And I have everything to do with it. No, seriously, you know who to blame if modules don't make it into the 1.3 release.
.js
.
jsnext:main
property to
your package.json
, so the tools know where
to look.
{ github, twitter, instagram, facebook }.com/benjamn ben@{benjamn,meteor}.com