From c21f096ab882dc37771375c4eadafe61a3e6ac51 Mon Sep 17 00:00:00 2001 From: "Alex Lam S.L" Date: Sun, 14 Feb 2021 20:13:54 +0000 Subject: [PATCH] support `export` statements (#4650) --- lib/ast.js | 95 +++++++++++++++++++++++++- lib/compress.js | 5 +- lib/output.js | 72 ++++++++++++++++++-- lib/parse.js | 117 ++++++++++++++++++++++++++++++- lib/scope.js | 23 +++++-- lib/transform.js | 9 +++ lib/utils.js | 6 +- test/compress/exports.js | 144 +++++++++++++++++++++++++++++++++++++++ test/mocha/exports.js | 71 +++++++++++++++++++ 9 files changed, 526 insertions(+), 16 deletions(-) create mode 100644 test/compress/exports.js create mode 100644 test/mocha/exports.js diff --git a/lib/ast.js b/lib/ast.js index 24b5a875..b386b442 100644 --- a/lib/ast.js +++ b/lib/ast.js @@ -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"); }, diff --git a/lib/compress.js b/lib/compress.js index f7151441..8bdc4b90 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -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; diff --git a/lib/output.js b/lib/output.js index 150f270a..2950e3cb 100644 --- a/lib/output.js +++ b/lib/output.js @@ -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) { diff --git a/lib/parse.js b/lib/parse.js index e287c40e..c6d7cb6f 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -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, diff --git a/lib/scope.js b/lib/scope.js index fb51486a..0f94d4e8 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -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); } diff --git a/lib/transform.js b/lib/transform.js index b6a0295a..bc08436e 100644 --- a/lib/transform.js +++ b/lib/transform.js @@ -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); diff --git a/lib/utils.js b/lib/utils.js index 81ddfa63..f9c78431 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -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 index 00000000..afa546ec --- /dev/null +++ b/test/compress/exports.js @@ -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 index 00000000..fa448ff0 --- /dev/null +++ b/test/mocha/exports.js @@ -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); + }); + }); +}); -- 2.34.1