diff --git a/lib/entry.js b/lib/entry.js index 2b179e03..a95f54c2 100644 --- a/lib/entry.js +++ b/lib/entry.js @@ -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; @@ -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; } } }; diff --git a/lib/import-export-visitor.js b/lib/import-export-visitor.js index efbc5537..b0321d95 100644 --- a/lib/import-export-visitor.js +++ b/lib/import-export-visitor.js @@ -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() { @@ -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 @@ -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; @@ -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) { diff --git a/lib/runtime.js b/lib/runtime.js index 4273cb82..e793a531 100644 --- a/lib/runtime.js +++ b/lib/runtime.js @@ -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; @@ -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 @@ -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, diff --git a/test/babel-plugin-tests.js b/test/babel-plugin-tests.js index f223b30e..cb6e8882 100644 --- a/test/babel-plugin-tests.js +++ b/test/babel-plugin-tests.js @@ -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; } diff --git a/test/compiler-tests.js b/test/compiler-tests.js index 6cca0383..aeaeca3d 100644 --- a/test/compiler-tests.js +++ b/test/compiler-tests.js @@ -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", () => { @@ -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" ); diff --git a/test/export-tests.js b/test/export-tests.js index 93ccf004..bc44f611 100644 --- a/test/export-tests.js +++ b/test/export-tests.js @@ -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); diff --git a/test/export/later.js b/test/export/later.js index a6089fb2..7453c8ac 100644 --- a/test/export/later.js +++ b/test/export/later.js @@ -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", diff --git a/test/output/anon-class/expected.js b/test/output/anon-class/expected.js index 83ea93b6..a066440a 100644 --- a/test/output/anon-class/expected.js +++ b/test/output/anon-class/expected.js @@ -1,5 +1,5 @@ -"use strict";module.export("default",exports.default=(class { +"use strict";module.exportDefault(class { constructor(value) { this.value = value; } -})); +}); diff --git a/test/output/default-expression/expected.js b/test/output/default-expression/expected.js index 752f698b..fb0d044a 100644 --- a/test/output/default-expression/expected.js +++ b/test/output/default-expression/expected.js @@ -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)); diff --git a/test/output/export-all/expected.js b/test/output/export-all/expected.js index fff7d932..fa924324 100644 --- a/test/output/export-all/expected.js +++ b/test/output/export-all/expected.js @@ -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"); diff --git a/test/setter/grandchild.js b/test/setter/grandchild.js index 40d0e14a..4e3c9b85 100644 --- a/test/setter/grandchild.js +++ b/test/setter/grandchild.js @@ -1,5 +1,5 @@ export let c = 0; export function increment() { ++c; - module.export(); + module.runSetters(); };