Skip to content

Commit

Permalink
Compile export default <expr> to module.exportDefault(<expr>).
Browse files Browse the repository at this point in the history
This allows us to simplify the module.export API significantly, since it
no longer sometimes takes a string as its first argument. Now `getters` is
always an object of methods that get the values of local variables, and
`constant` is an optional boolean to indicate that the getter functions
always return the same values, which helps with #134.
  • Loading branch information
benjamn committed May 26, 2017
1 parent e1c431f commit 2f8c2db
Show file tree
Hide file tree
Showing 11 changed files with 69 additions and 60 deletions.
3 changes: 2 additions & 1 deletion lib/entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Entry.getOrCreate = function (exported) {
return entry;
};

Ep.addGetters = function (getters) {
Ep.addGetters = function (getters, constant) {
var names = Object.keys(getters);
var nameCount = names.length;

Expand All @@ -81,6 +81,7 @@ Ep.addGetters = function (getters) {
// Should this throw if this.getters[name] exists?
! (name in this.getters)) {
this.getters[name] = getter;
getter.constant = !! constant;
}
}
};
Expand Down
65 changes: 34 additions & 31 deletions lib/import-export-visitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ const MagicString = require("./magic-string.js");
const Visitor = require("./visitor.js");

const codeOfCR = "\r".charCodeAt(0);
const exportDefaultPrefix = 'module.export("default",exports.default=(';
const exportDefaultSuffix = "));";

class ImportExportVisitor extends Visitor {
finalizeHoisting() {
Expand Down Expand Up @@ -214,15 +212,28 @@ class ImportExportVisitor extends Visitor {
}, "declaration");

} else {
// Otherwise, since the exported value is an expression, it's
// important that we wrap it with parentheses, in case it's something
// like a comma-separated sequence expression.
overwrite(this, decl.start, dd.start, exportDefaultPrefix);
// Otherwise, since the exported value is an expression, we use the
// special module.exportDefault(value) form.

path.call(this.visitWithoutReset, "declaration");
assert.strictEqual(decl.declaration, dd);

overwrite(this, dd.end, decl.end, exportDefaultSuffix, true);
let prefix = "module.exportDefault(";
let suffix = ");";

if (dd.type === "SequenceExpression") {
// If the exported expression is a comma-separated sequence
// expression, this.code.slice(dd.start, dd.end) may not include
// the vital parentheses, so we should wrap the expression with
// parentheses to make absolutely sure it is treated as a single
// argument to the module.exportDefault method, rather than as
// multiple arguments.
prefix += "(";
suffix = ")" + suffix;
}

overwrite(this, decl.start, dd.start, prefix);
overwrite(this, dd.end, decl.end, suffix, true);

if (this.modifyAST) {
// A Function or Class declaration has become an expression on the
Expand All @@ -234,7 +245,22 @@ class ImportExportVisitor extends Visitor {
dd.type = "ClassExpression";
}

path.replace(buildExportDefaultStatement(this, dd));
// Almost every JS parser parses this expression the same way, but
// we should still give custom parsers a chance to parse it.
let ast = this.parse("module.exportDefault(ARG);");
if (ast.type === "File") ast = ast.program;
assert.strictEqual(ast.type, "Program");

const callExprStmt = ast.body[0];
assert.strictEqual(callExprStmt.type, "ExpressionStatement");

const callExpr = callExprStmt.expression;
assert.strictEqual(callExpr.type, "CallExpression");

// Replace the ARG identifier with the exported expression.
callExpr.arguments[1] = dd;

path.replace(callExprStmt);
}

this.madeChanges = true;
Expand Down Expand Up @@ -349,29 +375,6 @@ function addToSpecifierMap(map, __ported, local) {
return map;
}

function buildExportDefaultStatement(visitor, declaration) {
let ast = visitor.parse(exportDefaultPrefix + "VALUE" + exportDefaultSuffix);

if (ast.type === "File") {
ast = ast.program;
}

assert.strictEqual(ast.type, "Program");

const stmt = ast.body[0];
assert.strictEqual(stmt.type, "ExpressionStatement");
assert.strictEqual(stmt.expression.type, "CallExpression");

const arg1 = stmt.expression.arguments[1];
assert.strictEqual(arg1.right.type, "Identifier");
assert.strictEqual(arg1.right.name, "VALUE");

// Replace the VALUE identifier with the desired declaration.
arg1.right = declaration;

return stmt;
}

// Returns a map from {im,ex}ported identifiers to lists of local variable
// names bound to those identifiers.
function computeSpecifierMap(specifiers) {
Expand Down
24 changes: 15 additions & 9 deletions lib/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ exports.enable = function (mod) {
if (typeof mod.export !== "function" ||
typeof mod.importSync !== "function") {
mod.export = moduleExport;
mod.watch = moduleWatch;
mod.exportDefault = moduleExportDefault;
mod.runSetters = runSetters;
mod.watch = moduleWatch;

// To be deprecated:
mod.runModuleSetters = runSetters;
Expand Down Expand Up @@ -40,16 +41,12 @@ function importSync(id, setters, key) {
}

// Register getter functions for local variables in the scope of an export
// statement. The keys of the getters object are exported names, and the
// values are functions that return local values.
function moduleExport(getters) {
// statement. Pass true as the second argument to indicate that the getter
// functions always return the same values.
function moduleExport(getters, constant) {
utils.setESModule(this.exports);
var entry = Entry.getOrCreate(this.exports);

if (utils.isObject(getters)) {
entry.addGetters(getters);
}

entry.addGetters(getters, constant);
if (this.loaded) {
// If the module has already been evaluated, then we need to trigger
// another round of entry.runSetters calls, which begins by calling
Expand All @@ -58,6 +55,15 @@ function moduleExport(getters) {
}
}

// Register a getter function that always returns the given value.
function moduleExportDefault(value) {
return this.export({
default: function () {
return value;
}
}, true);
}

// Platform-specific code should find a way to call this method whenever
// the module system is about to return module.exports from require. This
// might happen more than once per module, in case of dependency cycles,
Expand Down
2 changes: 1 addition & 1 deletion test/babel-plugin-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe("babel-plugin-transform-es2015-modules-reify", () => {
const ast = parse(code);
delete ast.tokens;
const result = transformFromAst(ast, code, options);
assert.ok(/\bmodule\.(?:watch|importSync|export)\b/.test(result.code));
assert.ok(/\bmodule\.(?:export(?:Default)?|import(?:Sync)?|watch)\b/.test(result.code));
return result;
}

Expand Down
4 changes: 2 additions & 2 deletions test/compiler-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ describe("compiler", () => {
isVarDecl(ast.body[firstIndex + 2], ["bar"]);
isCallExprStmt(ast.body[firstIndex + 3], "module", "watch");
isCallExprStmt(ast.body[firstIndex + 4], "console", "log");
isCallExprStmt(ast.body[firstIndex + 5], "module", "export");
isCallExprStmt(ast.body[firstIndex + 5], "module", "exportDefault");
});

it("should respect options.enforceStrictMode", () => {
Expand Down Expand Up @@ -255,7 +255,7 @@ describe("compiler", () => {

assert.strictEqual(anonAST.body.length - anonFirstIndex, 1);
assert.strictEqual(
anonAST.body[anonFirstIndex].expression.arguments[1].right.type,
anonAST.body[anonFirstIndex].expression.arguments[1].type,
"ClassExpression"
);

Expand Down
8 changes: 2 additions & 6 deletions test/export-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,12 @@ describe("export declarations", () => {
assert.strictEqual(val, "value-1");

exportAgain();
assert.strictEqual(def, "default-2");
assert.strictEqual(val, "value-2");

exportYetAgain();
assert.strictEqual(def, "default-3");
assert.strictEqual(def, "default-1");
assert.strictEqual(val, "value-2");

setTimeout(() => {
oneLastExport();
assert.strictEqual(def, "default-3");
assert.strictEqual(def, "default-1");
assert.strictEqual(val, "value-3");
done();
}, 0);
Expand Down
13 changes: 8 additions & 5 deletions test/export/later.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ export default "default-1";
export let val = "value-1";

export function exportAgain() {
module.export("default", exports.default = "default-2");
// Neither of these re-export styles should work, because the original
// export default still takes precedence over anything else.
module.exportDefault(exports.default = "default-2");

// This style also does not work, because the getter function for the
// variable val is all that matters.
exports.val = "ignored";

val = +(val.split("-")[1]);
val = "value-" + ++val;
}

export function exportYetAgain() {
module.export("default", exports.default = "default-3");
}

export function oneLastExport() {
strictEqual(
val = "value-3",
Expand Down
4 changes: 2 additions & 2 deletions test/output/anon-class/expected.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use strict";module.export("default",exports.default=(class {
"use strict";module.exportDefault(class {
constructor(value) {
this.value = value;
}
}));
});
2 changes: 1 addition & 1 deletion test/output/default-expression/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

// This default expression will evaluate to 0 if the parentheses are
// mistakenly stripped away.
module.export("default",exports.default=(count++, count));
module.exportDefault((count++, count));
2 changes: 1 addition & 1 deletion test/output/export-all/expected.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
"use strict";module.watch(require("./abc"),{"*":function(v,k){exports[k]=v}},0);
module.export("default",exports.default=("default"));
module.exportDefault("default");
2 changes: 1 addition & 1 deletion test/setter/grandchild.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export let c = 0;
export function increment() {
++c;
module.export();
module.runSetters();
};

0 comments on commit 2f8c2db

Please sign in to comment.