support `export` statements (#4650)
authorAlex Lam S.L <alexlamsl@gmail.com>
Sun, 14 Feb 2021 20:13:54 +0000 (20:13 +0000)
committerGitHub <noreply@github.com>
Sun, 14 Feb 2021 20:13:54 +0000 (04:13 +0800)
lib/ast.js
lib/compress.js
lib/output.js
lib/parse.js
lib/scope.js
lib/transform.js
lib/utils.js
test/compress/exports.js [new file with mode: 0644]
test/mocha/exports.js [new file with mode: 0644]

index 24b5a87..b386b44 100644 (file)
@@ -207,6 +207,7 @@ var AST_Directive = DEFNODE("Directive", "quote value", {
     _validate: function() {
         if (this.quote != null) {
             if (typeof this.quote != "string") throw new Error("quote must be string");
+            if (!/^["']$/.test(this.quote)) throw new Error("invalid quote: " + this.quote);
         }
         if (typeof this.value != "string") throw new Error("value must be string");
     },
@@ -238,7 +239,7 @@ function must_be_expression(node, prop) {
 var AST_SimpleStatement = DEFNODE("SimpleStatement", "body", {
     $documentation: "A statement consisting of an expression, i.e. a = 1 + 2",
     $propdoc: {
-        body: "[AST_Node] an expression node (should not be instanceof AST_Statement)"
+        body: "[AST_Node] an expression node (should not be instanceof AST_Statement)",
     },
     walk: function(visitor) {
         var node = this;
@@ -1038,6 +1039,86 @@ var AST_VarDef = DEFNODE("VarDef", "name value", {
 
 /* -----[ OTHER ]----- */
 
+var AST_ExportDeclaration = DEFNODE("ExportDeclaration", "body", {
+    $documentation: "An `export` statement",
+    $propdoc: {
+        body: "[AST_Definitions|AST_LambdaDefinition] the statement to export",
+    },
+    walk: function(visitor) {
+        var node = this;
+        visitor.visit(node, function() {
+            node.body.walk(visitor);
+        });
+    },
+    _validate: function() {
+        if (!(this.body instanceof AST_Definitions || this.body instanceof AST_LambdaDefinition)) {
+            throw new Error("body must be AST_Definitions or AST_LambdaDefinition");
+        }
+    },
+}, AST_Statement);
+
+var AST_ExportDefault = DEFNODE("ExportDefault", "body", {
+    $documentation: "An `export default` statement",
+    $propdoc: {
+        body: "[AST_Node] an expression node (should not be instanceof AST_Statement)",
+    },
+    walk: function(visitor) {
+        var node = this;
+        visitor.visit(node, function() {
+            node.body.walk(visitor);
+        });
+    },
+    _validate: function() {
+        must_be_expression(this, "body");
+    },
+}, AST_Statement);
+
+var AST_ExportForeign = DEFNODE("ExportForeign", "aliases keys path quote", {
+    $documentation: "An `export ... from '...'` statement",
+    $propdoc: {
+        aliases: "[string*] array of aliases to export",
+        keys: "[string*] array of keys to import",
+        path: "[string] the path to import module",
+        quote: "[string?] the original quote character",
+    },
+    _validate: function() {
+        if (this.aliases.length != this.keys.length) {
+            throw new Error("aliases:key length mismatch: " + this.aliases.length + " != " + this.keys.length);
+        }
+        this.aliases.forEach(function(name) {
+            if (typeof name != "string") throw new Error("aliases must contain string");
+        });
+        this.keys.forEach(function(name) {
+            if (typeof name != "string") throw new Error("keys must contain string");
+        });
+        if (typeof this.path != "string") throw new Error("path must be string");
+        if (this.quote != null) {
+            if (typeof this.quote != "string") throw new Error("quote must be string");
+            if (!/^["']$/.test(this.quote)) throw new Error("invalid quote: " + this.quote);
+        }
+    },
+}, AST_Statement);
+
+var AST_ExportReferences = DEFNODE("ExportReferences", "properties", {
+    $documentation: "An `export { ... }` statement",
+    $propdoc: {
+        properties: "[AST_SymbolExport*] array of aliases to export",
+    },
+    walk: function(visitor) {
+        var node = this;
+        visitor.visit(node, function() {
+            node.properties.forEach(function(prop) {
+                prop.walk(visitor);
+            });
+        });
+    },
+    _validate: function() {
+        this.properties.forEach(function(prop) {
+            if (!(prop instanceof AST_SymbolExport)) throw new Error("properties must contain AST_SymbolExport");
+        });
+    },
+}, AST_Statement);
+
 var AST_Import = DEFNODE("Import", "all default path properties quote", {
     $documentation: "An `import` statement",
     $propdoc: {
@@ -1072,6 +1153,7 @@ var AST_Import = DEFNODE("Import", "all default path properties quote", {
         });
         if (this.quote != null) {
             if (typeof this.quote != "string") throw new Error("quote must be string");
+            if (!/^["']$/.test(this.quote)) throw new Error("invalid quote: " + this.quote);
         }
     },
 }, AST_Statement);
@@ -1572,6 +1654,16 @@ var AST_SymbolRef = DEFNODE("SymbolRef", "fixed in_arg redef", {
     $documentation: "Reference to some symbol (not definition/declaration)",
 }, AST_Symbol);
 
+var AST_SymbolExport = DEFNODE("SymbolExport", "alias", {
+    $documentation: "Reference in an `export` statement",
+    $propdoc: {
+        alias: "[string] the `export` alias",
+    },
+    _validate: function() {
+        if (typeof this.alias != "string") throw new Error("alias must be string");
+    },
+}, AST_SymbolRef);
+
 var AST_LabelRef = DEFNODE("LabelRef", null, {
     $documentation: "Reference to a label symbol",
 }, AST_Symbol);
@@ -1627,6 +1719,7 @@ var AST_String = DEFNODE("String", "quote value", {
     _validate: function() {
         if (this.quote != null) {
             if (typeof this.quote != "string") throw new Error("quote must be string");
+            if (!/^["']$/.test(this.quote)) throw new Error("invalid quote: " + this.quote);
         }
         if (typeof this.value != "string") throw new Error("value must be string");
     },
index f715144..8bdc4b9 100644 (file)
@@ -173,6 +173,7 @@ Compressor.prototype = new TreeTransformer;
 merge(Compressor.prototype, {
     option: function(key) { return this.options[key] },
     exposed: function(def) {
+        if (def.exported) return true;
         if (def.undeclared) return true;
         if (!(def.global || def.scope.resolve() instanceof AST_Toplevel)) return false;
         var toplevel = this.toplevel;
@@ -5583,7 +5584,7 @@ merge(Compressor.prototype, {
             if (scope === self) {
                 if (node instanceof AST_LambdaDefinition) {
                     var def = node.name.definition();
-                    if (!drop_funcs && !(def.id in in_use_ids)) {
+                    if ((!drop_funcs || def.exported) && !(def.id in in_use_ids)) {
                         in_use_ids[def.id] = true;
                         in_use.push(def);
                     }
@@ -5602,7 +5603,7 @@ merge(Compressor.prototype, {
                                 var redef = def.redefined();
                                 if (redef) var_defs[redef.id] = (var_defs[redef.id] || 0) + 1;
                             }
-                            if (!(def.id in in_use_ids) && (!drop_vars
+                            if (!(def.id in in_use_ids) && (!drop_vars || def.exported
                                 || (node instanceof AST_Const ? def.redefined() : def.const_redefs)
                                 || !(node instanceof AST_Var || is_safe_lexical(def)))) {
                                 in_use_ids[def.id] = true;
index 150f270..2950e3c 100644 (file)
@@ -892,10 +892,6 @@ function OutputStream(options) {
         use_asm = was_asm;
     }
 
-    DEFPRINT(AST_Statement, function(output) {
-        this.body.print(output);
-        output.semicolon();
-    });
     DEFPRINT(AST_Toplevel, function(output) {
         display_body(this.body, true, output, true);
         output.print("");
@@ -1011,6 +1007,64 @@ function OutputStream(options) {
         output.space();
         force_statement(self.body, output);
     });
+    DEFPRINT(AST_ExportDeclaration, function(output) {
+        output.print("export");
+        output.space();
+        this.body.print(output);
+    });
+    DEFPRINT(AST_ExportDefault, function(output) {
+        output.print("export");
+        output.space();
+        output.print("default");
+        output.space();
+        this.body.print(output);
+        output.semicolon();
+    });
+    DEFPRINT(AST_ExportForeign, function(output) {
+        var self = this;
+        output.print("export");
+        output.space();
+        var len = self.keys.length;
+        if (len == 0) {
+            print_braced_empty(self, output);
+        } else if (self.keys[0] == "*") {
+            print_entry(0);
+        } else output.with_block(function() {
+            output.indent();
+            print_entry(0);
+            for (var i = 1; i < len; i++) {
+                output.print(",");
+                output.newline();
+                output.indent();
+                print_entry(i);
+            }
+            output.newline();
+        });
+        output.space();
+        output.print("from");
+        output.space();
+        output.print_string(self.path, self.quote);
+        output.semicolon();
+
+        function print_entry(index) {
+            var alias = self.aliases[index];
+            var key = self.keys[index];
+            output.print_name(key);
+            if (alias != key) {
+                output.space();
+                output.print("as");
+                output.space();
+                output.print_name(alias);
+            }
+        }
+    });
+    DEFPRINT(AST_ExportReferences, function(output) {
+        var self = this;
+        output.print("export");
+        output.space();
+        print_properties(self, output);
+        output.semicolon();
+    });
     DEFPRINT(AST_Import, function(output) {
         var self = this;
         output.print("import");
@@ -1543,6 +1597,16 @@ function OutputStream(options) {
     DEFPRINT(AST_Symbol, function(output) {
         print_symbol(this, output);
     });
+    DEFPRINT(AST_SymbolExport, function(output) {
+        var self = this;
+        print_symbol(self, output);
+        if (self.alias) {
+            output.space();
+            output.print("as");
+            output.space();
+            output.print_name(self.alias);
+        }
+    });
     DEFPRINT(AST_SymbolImport, function(output) {
         var self = this;
         if (self.key) {
index e287c40..c6d7cb6 100644 (file)
@@ -844,6 +844,9 @@ function parse($TEXT, options) {
               case "await":
                 if (S.in_async) return simple_statement();
                 break;
+              case "export":
+                next();
+                return export_();
               case "import":
                 next();
                 return import_();
@@ -1275,6 +1278,115 @@ function parse($TEXT, options) {
         });
     }
 
+    function is_alias() {
+        return is("name") || is_identifier_string(S.token.value);
+    }
+
+    function export_() {
+        if (is("operator", "*")) {
+            next();
+            var alias = "*";
+            if (is("name", "as")) {
+                next();
+                if (!is_alias()) expect_token("name");
+                alias = S.token.value;
+                next();
+            }
+            expect_token("name", "from");
+            var path = S.token;
+            expect_token("string");
+            semicolon();
+            return new AST_ExportForeign({
+                aliases: [ alias ],
+                keys: [ "*" ],
+                path: path.value,
+                quote: path.quote,
+            });
+        }
+        if (is("punc", "{")) {
+            next();
+            var aliases = [];
+            var keys = [];
+            while (is_alias()) {
+                var key = S.token;
+                next();
+                keys.push(key);
+                if (is("name", "as")) {
+                    next();
+                    if (!is_alias()) expect_token("name");
+                    aliases.push(S.token.value);
+                    next();
+                } else {
+                    aliases.push(key.value);
+                }
+                if (!is("punc", "}")) expect(",");
+            }
+            expect("}");
+            if (is("name", "from")) {
+                next();
+                var path = S.token;
+                expect_token("string");
+                semicolon();
+                return new AST_ExportForeign({
+                    aliases: aliases,
+                    keys: keys.map(function(token) {
+                        return token.value;
+                    }),
+                    path: path.value,
+                    quote: path.quote,
+                });
+            }
+            semicolon();
+            return new AST_ExportReferences({
+                properties: keys.map(function(token, index) {
+                    if (!is_token(token, "name")) token_error(token, "Name expected");
+                    var sym = _make_symbol(AST_SymbolExport, token);
+                    sym.alias = aliases[index];
+                    return sym;
+                }),
+            });
+        }
+        if (is("keyword", "default")) {
+            next();
+            var body = expression();
+            semicolon();
+            return new AST_ExportDefault({ body: body });
+        }
+        return new AST_ExportDeclaration({ body: export_decl() });
+    }
+
+    var export_decl = embed_tokens(function() {
+        switch (S.token.value) {
+          case "async":
+            next();
+            expect_token("keyword", "function");
+            if (!is("operator", "*")) return function_(AST_AsyncDefun);
+            next();
+            return function_(AST_AsyncGeneratorDefun);
+          case "const":
+            next();
+            var node = const_();
+            semicolon();
+            return node;
+          case "function":
+            next();
+            if (!is("operator", "*")) return function_(AST_Defun);
+            next();
+            return function_(AST_GeneratorDefun);
+          case "let":
+            next();
+            var node = let_();
+            semicolon();
+            return node;
+          case "var":
+            next();
+            var node = var_();
+            semicolon();
+            return node;
+        }
+        unexpected();
+    });
+
     function import_() {
         var all = null;
         var def = as_symbol(AST_SymbolImport, true);
@@ -1288,7 +1400,7 @@ function parse($TEXT, options) {
             } else {
                 expect("{");
                 props = [];
-                while (is("name") || is_identifier_string(S.token.value)) {
+                while (is_alias()) {
                     var alias;
                     if (is_token(peek(), "name", "as")) {
                         var key = S.token.value;
@@ -1307,9 +1419,8 @@ function parse($TEXT, options) {
             }
         }
         if (all || def || props) expect_token("name", "from");
-        if (!is("string")) unexpected();
         var path = S.token;
-        next();
+        expect_token("string");
         semicolon();
         return new AST_Import({
             all: all,
index fb51486..0f94d4e 100644 (file)
@@ -45,6 +45,7 @@
 
 function SymbolDef(id, scope, orig, init) {
     this.eliminated = 0;
+    this.exported = false;
     this.global = false;
     this.id = id;
     this.init = init;
@@ -91,6 +92,7 @@ SymbolDef.prototype = {
     },
     unmangleable: function(options) {
         return this.global && !options.toplevel
+            || this.exported
             || this.undeclared
             || !options.eval && this.scope.pinned()
             || options.keep_fnames
@@ -118,11 +120,22 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options) {
     // pass 1: setup scope chaining and handle definitions
     var self = this;
     var defun = null;
+    var exported = false;
     var next_def_id = 0;
     var scope = self.parent_scope = null;
     var tw = new TreeWalker(function(node, descend) {
+        if (node instanceof AST_Definitions) {
+            var save_exported = exported;
+            exported = tw.parent() instanceof AST_ExportDeclaration;
+            descend();
+            exported = save_exported;
+            return true;
+        }
         if (node instanceof AST_LambdaDefinition) {
+            var save_exported = exported;
+            exported = tw.parent() instanceof AST_ExportDeclaration;
             node.name.walk(tw);
+            exported = save_exported;
             walk_scope(function() {
                 node.argnames.forEach(function(argname) {
                     argname.walk(tw);
@@ -169,9 +182,11 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options) {
         if (node instanceof AST_SymbolCatch) {
             scope.def_variable(node).defun = defun;
         } else if (node instanceof AST_SymbolConst) {
-            scope.def_variable(node).defun = defun;
+            var def = scope.def_variable(node);
+            def.defun = defun;
+            def.exported = exported;
         } else if (node instanceof AST_SymbolDefun) {
-            defun.def_function(node, tw.parent());
+            defun.def_function(node, tw.parent()).exported = exported;
             entangle(defun, scope);
         } else if (node instanceof AST_SymbolFunarg) {
             defun.def_variable(node);
@@ -180,9 +195,9 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options) {
             var def = defun.def_function(node, node.name == "arguments" ? undefined : defun);
             if (options.ie8) def.defun = defun.parent_scope.resolve();
         } else if (node instanceof AST_SymbolLet) {
-            scope.def_variable(node);
+            scope.def_variable(node).exported = exported;
         } else if (node instanceof AST_SymbolVar) {
-            defun.def_variable(node, null);
+            defun.def_variable(node, null).exported = exported;
             entangle(defun, scope);
         }
 
index b6a0295..bc08436 100644 (file)
@@ -204,6 +204,15 @@ TreeTransformer.prototype = new TreeWalker;
         if (self.key instanceof AST_Node) self.key = self.key.transform(tw);
         self.value = self.value.transform(tw);
     });
+    DEF(AST_ExportDeclaration, function(self, tw) {
+        self.body = self.body.transform(tw);
+    });
+    DEF(AST_ExportDefault, function(self, tw) {
+        self.body = self.body.transform(tw);
+    });
+    DEF(AST_ExportReferences, function(self, tw) {
+        self.properties = do_list(self.properties, tw);
+    });
     DEF(AST_Import, function(self, tw) {
         if (self.all) self.all = self.all.transform(tw);
         if (self.default) self.default = self.default.transform(tw);
index 81ddfa6..f9c7843 100644 (file)
@@ -249,12 +249,14 @@ function first_in_statement(stack, arrow) {
             if (p.expression === node) continue;
         } else if (p instanceof AST_Conditional) {
             if (p.condition === node) continue;
+        } else if (p instanceof AST_ExportDefault) {
+            return false;
         } else if (p instanceof AST_PropAccess) {
             if (p.expression === node) continue;
         } else if (p instanceof AST_Sequence) {
             if (p.expressions[0] === node) continue;
-        } else if (p instanceof AST_Statement) {
-            return p.body === node;
+        } else if (p instanceof AST_SimpleStatement) {
+            return true;
         } else if (p instanceof AST_Template) {
             if (p.tag === node) continue;
         } else if (p instanceof AST_UnaryPostfix) {
diff --git a/test/compress/exports.js b/test/compress/exports.js
new file mode 100644 (file)
index 0000000..afa546e
--- /dev/null
@@ -0,0 +1,144 @@
+refs: {
+    input: {
+        export {};
+        export { a, b as B, c as case, d as default };
+    }
+    expect_exact: "export{};export{a as a,b as B,c as case,d as default};"
+}
+
+var_defs: {
+    input: {
+        export const a = 1;
+        export let b = 2, c = 3;
+        export var { d, e: [] } = f;
+    }
+    expect_exact: "export const a=1;export let b=2,c=3;export var{d:d,e:[]}=f;"
+}
+
+defuns: {
+    input: {
+        export function e() {}
+        export function* f(a) {}
+        export async function g(b, c) {}
+        export async function* h({}, ...[]) {}
+    }
+    expect_exact: "export function e(){}export function*f(a){}export async function g(b,c){}export async function*h({},...[]){}"
+}
+
+defaults: {
+    input: {
+        export default 42;
+        export default (x, y) => x * x;
+        export default function*(a, b) {};
+        export default async function f({ c }, ...[ d ]) {};
+    }
+    expect_exact: "export default 42;export default(x,y)=>x*x;export default function*(a,b){};export default async function f({c:c},...[d]){};"
+}
+
+foreign: {
+    input: {
+        export * from "foo";
+        export {} from "bar";
+        export * as a from "baz";
+        export { default } from "moo";
+        export { b, c as case, default as delete, d } from "moz";
+    }
+    expect_exact: 'export*from"foo";export{}from"bar";export*as a from"baz";export{default}from"moo";export{b,c as case,default as delete,d}from"moz";'
+}
+
+same_quotes: {
+    beautify = {
+        beautify: true,
+        quote_style: 3,
+    }
+    input: {
+        export * from 'foo';
+        export {} from "bar";
+    }
+    expect_exact: [
+        "export * from 'foo';",
+        "",
+        'export {} from "bar";',
+    ]
+}
+
+drop_unused: {
+    options = {
+        toplevel: true,
+        unused: true,
+    }
+    input: {
+        export default 42;
+        export default (x, y) => x * x;
+        export default function*(a, b) {};
+        export default async function f({ c }, ...[ d ]) {};
+        export var e;
+        export function g(x, [ y ], ...z) {}
+    }
+    expect: {
+        export default 42;
+        export default (x, y) => x * x;
+        export default function*(a, b) {};
+        export default async function({}) {};
+        export var e;
+        export function g(x, []) {}
+    }
+}
+
+mangle: {
+    rename = false
+    mangle = {
+        toplevel: true,
+    }
+    input: {
+        const a = 42;
+        export let b, { foo: c } = a;
+        export function f(d, { [b]: e }) {
+            d(e, f);
+        }
+        export default a;
+        export default async function g(x, ...{ [c]: y }) {
+            (await x)(g, y);
+        }
+    }
+    expect: {
+        const t = 42;
+        export let b, { foo: c } = t;
+        export function f(t, { [b]: o }) {
+            t(o, f);
+        }
+        export default t;
+        export default async function t(o, ...{ [c]: e}) {
+            (await o)(t, e);
+        }
+    }
+}
+
+mangle_rename: {
+    rename = true
+    mangle = {
+        toplevel: true,
+    }
+    input: {
+        const a = 42;
+        export let b, { foo: c } = a;
+        export function f(d, { [b]: e }) {
+            d(e, f);
+        }
+        export default a;
+        export default async function g(x, ...{ [c]: y }) {
+            (await x)(g, y);
+        }
+    }
+    expect: {
+        const t = 42;
+        export let b, { foo: c } = t;
+        export function f(t, { [b]: o }) {
+            t(o, f);
+        }
+        export default t;
+        export default async function t(o, ...{ [c]: e}) {
+            (await o)(t, e);
+        }
+    }
+}
diff --git a/test/mocha/exports.js b/test/mocha/exports.js
new file mode 100644 (file)
index 0000000..fa448ff
--- /dev/null
@@ -0,0 +1,71 @@
+var assert = require("assert");
+var UglifyJS = require("../node");
+
+describe("export", function() {
+    it("Should reject invalid `export ...` statement syntax", function() {
+        [
+            "export *;",
+            "export A;",
+            "export 42;",
+            "export var;",
+            "export * as A;",
+            "export A as B;",
+            "export const A;",
+            "export function(){};",
+        ].forEach(function(code) {
+            assert.throws(function() {
+                UglifyJS.parse(code);
+            }, function(e) {
+                return e instanceof UglifyJS.JS_Parse_Error;
+            }, code);
+        });
+    });
+    it("Should reject invalid `export { ... }` statement syntax", function() {
+        [
+            "export { * };",
+            "export { * as A };",
+            "export { 42 as A };",
+            "export { A as B-C };",
+            "export { default as A };",
+        ].forEach(function(code) {
+            assert.throws(function() {
+                UglifyJS.parse(code);
+            }, function(e) {
+                return e instanceof UglifyJS.JS_Parse_Error;
+            }, code);
+        });
+    });
+    it("Should reject invalid `export default ...` statement syntax", function() {
+        [
+            "export default *;",
+            "export default var;",
+            "export default A as B;",
+        ].forEach(function(code) {
+            assert.throws(function() {
+                UglifyJS.parse(code);
+            }, function(e) {
+                return e instanceof UglifyJS.JS_Parse_Error;
+            }, code);
+        });
+    });
+    it("Should reject invalid `export ... from ...` statement syntax", function() {
+        [
+            "export from 'path';",
+            "export * from `path`;",
+            "export A as B from 'path';",
+            "export default from 'path';",
+            "export { A }, B from 'path';",
+            "export * as A, B from 'path';",
+            "export * as A, {} from 'path';",
+            "export { * as A } from 'path';",
+            "export { 42 as A } from 'path';",
+            "export { A-B as C } from 'path';",
+        ].forEach(function(code) {
+            assert.throws(function() {
+                UglifyJS.parse(code);
+            }, function(e) {
+                return e instanceof UglifyJS.JS_Parse_Error;
+            }, code);
+        });
+    });
+});