diff --git a/lib/ast.js b/lib/ast.js index fabe7b40..54e3f2e6 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -789,11 +789,13 @@ var AST_Import = DEFNODE("Import", "imported_name imported_names module_name", { } }); -var AST_Export = DEFNODE("Export", "exported_definition exported_value is_default", { +var AST_Export = DEFNODE("Export", "exported_definition exported_value is_default exported_names module_name", { $documentation: "An `export` statement", $propdoc: { exported_definition: "[AST_Defun|AST_Definitions|AST_DefClass?] An exported definition", exported_value: "[AST_Node?] An exported value", + exported_names: "[AST_NameImport*?] List of exported names", + module_name: "[AST_String?] Name of the file to load exports from", is_default: "[Boolean] Whether this is the default exported value of this module" }, _walk: function (visitor) { diff --git a/lib/output.js b/lib/output.js index b174c08b..01fbdd13 100644 --- a/lib/output.js +++ b/lib/output.js @@ -1277,11 +1277,37 @@ function OutputStream(options) { output.print("default"); output.space(); } - if (self.exported_value) { + if (self.exported_names) { + output.space(); + + if (self.exported_names.length === 1 && self.exported_names[0].name.name === "*") { + self.exported_names[0].print(output); + } else { + output.print("{"); + self.exported_names.forEach(function (name_import, i) { + output.space(); + name_import.print(output); + if (i < self.exported_names.length - 1) { + output.print(","); + output.space(); + } + }); + output.space(); + output.print("}"); + } + output.space(); + } + else if (self.exported_value) { self.exported_value.print(output); } else if (self.exported_definition) { self.exported_definition.print(output); } + if (self.module_name) { + output.space(); + output.print("from"); + output.space(); + self.module_name.print(output); + } output.semicolon(); }); diff --git a/lib/parse.js b/lib/parse.js index 3b167044..c2a07b84 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -2237,17 +2237,85 @@ function parse($TEXT, options) { }) } + function import_nameAsterisk() { + var start = S.token; + var foreign_name; + var name; + + next(); + + var end = prev(); + + name = new AST_SymbolImport({ + name: '*', + start: start, + end: end, + }); + + foreign_name = new AST_SymbolImportForeign({ + name: '*', + start: start, + end: end, + }); + + return new AST_NameImport({ + start: start, + foreign_name: foreign_name, + name: name, + end: end, + }) + } + function export_() { var start = S.token; var is_default; var exported_value; var exported_definition; + var exported_names; if (is("keyword", "default")) { is_default = true; next(); } + if (is("punc", "{")) { + next(); + exported_names = []; + while (!is("punc", "}")) { + exported_names.push(import_name()); + if (is("punc", ",")) { + next(); + } + } + next(); + } else if (is("operator", "*")) { + var st = prev(); + exported_names = [import_nameAsterisk()]; + } + + if (exported_names) { + expect_token("name", "from"); + + var mod_str = S.token; + if (mod_str.type !== 'string') { + unexpected(); + } + next(); + + return new AST_Export({ + start: start, + is_default: is_default, + exported_names: exported_names, + module_name: new AST_String({ + start: mod_str, + value: mod_str.value, + quote: mod_str.quote, + end: mod_str, + }), + end: prev(), + }); + } + var is_definition = is("keyword", "var") || is("keyword", "let") || is("keyword", "const") || is("keyword", "class") || is("keyword", "function"); diff --git a/test/compress/harmony.js b/test/compress/harmony.js index 8b8b8d10..bd84221b 100644 --- a/test/compress/harmony.js +++ b/test/compress/harmony.js @@ -199,6 +199,15 @@ export_statement: { expect_exact: "export default 1;export var foo=4;export let foo=6;export const foo=6;export function foo(){};export class foo{};" } +export_module_statement: { + input: { + export * from "a.js"; + export {A} from "a.js"; + export {A, B} from "a.js"; + } + expect_exact: 'export*from"a.js";export{A}from"a.js";export{A,B}from"a.js";' +} + import_statement_mangling: { mangle = { toplevel: true }; input: { diff --git a/test/mocha/export.js b/test/mocha/export.js new file mode 100644 index 00000000..317a076a --- /dev/null +++ b/test/mocha/export.js @@ -0,0 +1,40 @@ +var assert = require("assert"); +var uglify = require("../../"); + +describe("Export", function() { + it ("Should parse export directives", function() { + + var inputs = [ + ['export * from "a.js"', ['*'], "a.js"], + ['export {A} from "a.js"', ['A'], "a.js"], + ['export {A as X} from "a.js"', ['X'], "a.js"], + ['export {A as Foo, B} from "a.js"', ['Foo', 'B'], "a.js"], + ['export {A, B} from "a.js"', ['A', 'B'], "a.js"], + ]; + + var test = function(code) { + return uglify.parse(code, {fromString: true}); + }; + + var extractNames = function(symbols) { + var ret = []; + for (var i = 0; i < symbols.length; i++) { + ret.push(symbols[i].name.name) + } + return ret; + }; + + for (var i = 0; i < inputs.length; i++) { + var ast = test(inputs[i][0]); + var names = inputs[i][1]; + var filename = inputs[i][2]; + assert(ast instanceof uglify.AST_Toplevel); + assert.equal(ast.body.length, 1); + var st = ast.body[0]; + assert(st instanceof uglify.AST_Export); + var actualNames = extractNames(st.exported_names); + assert.deepEqual(actualNames, names); + assert.equal(st.module_name.value, filename) + } + }) +}); \ No newline at end of file diff --git a/test/mocha/issue1702.js b/test/mocha/issue1702.js new file mode 100644 index 00000000..4eb1b989 --- /dev/null +++ b/test/mocha/issue1702.js @@ -0,0 +1,35 @@ +var uglify = require('../../'); +var assert = require("assert"); + +describe("For statement", function() { + it("For variable should list enclosing scope in its references (issue #17022)", function() { + var ast = uglify.parse("function f() { for (var a = 0; a < 10; a++) {} }"); + ast.figure_out_scope(); + + var checkWalker = new uglify.TreeWalker(function(node, descend) { + if (node instanceof uglify.AST_VarDef) { + console.log("AST_VarDef"); + // one reference should be in the AST_Defun scope - search for it + + var walkNode = function (r) { + console.log(r.CTOR.name); + var walker = new uglify.TreeWalker(function(node, descend){ + // do not walk into any other scope, it should be listed if needed + console.log(" " + node.CTOR.name); + if (node instanceof uglify.AST_Scope && node != r.scope) return true; + if (node instanceof uglify.AST_For) { + console.log("Great - we found the for statement referencing the variable") + } + return false; + }); + r.scope.walk(walker); + r.walk(walker); + }; + + node.name.thedef.orig.forEach(walkNode); + node.name.thedef.references.forEach(walkNode); + } + }); + ast.walk(checkWalker); + }); +}); \ No newline at end of file