support export statements (#4650)

This commit is contained in:
Alex Lam S.L
2021-02-14 20:13:54 +00:00
committed by GitHub
parent b7219ac489
commit c21f096ab8
9 changed files with 526 additions and 16 deletions

View File

@@ -207,6 +207,7 @@ var AST_Directive = DEFNODE("Directive", "quote value", {
_validate: function() { _validate: function() {
if (this.quote != null) { if (this.quote != null) {
if (typeof this.quote != "string") throw new Error("quote must be string"); 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"); 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", { var AST_SimpleStatement = DEFNODE("SimpleStatement", "body", {
$documentation: "A statement consisting of an expression, i.e. a = 1 + 2", $documentation: "A statement consisting of an expression, i.e. a = 1 + 2",
$propdoc: { $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) { walk: function(visitor) {
var node = this; var node = this;
@@ -1038,6 +1039,86 @@ var AST_VarDef = DEFNODE("VarDef", "name value", {
/* -----[ OTHER ]----- */ /* -----[ 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", { var AST_Import = DEFNODE("Import", "all default path properties quote", {
$documentation: "An `import` statement", $documentation: "An `import` statement",
$propdoc: { $propdoc: {
@@ -1072,6 +1153,7 @@ var AST_Import = DEFNODE("Import", "all default path properties quote", {
}); });
if (this.quote != null) { if (this.quote != null) {
if (typeof this.quote != "string") throw new Error("quote must be string"); 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); }, AST_Statement);
@@ -1572,6 +1654,16 @@ var AST_SymbolRef = DEFNODE("SymbolRef", "fixed in_arg redef", {
$documentation: "Reference to some symbol (not definition/declaration)", $documentation: "Reference to some symbol (not definition/declaration)",
}, AST_Symbol); }, 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, { var AST_LabelRef = DEFNODE("LabelRef", null, {
$documentation: "Reference to a label symbol", $documentation: "Reference to a label symbol",
}, AST_Symbol); }, AST_Symbol);
@@ -1627,6 +1719,7 @@ var AST_String = DEFNODE("String", "quote value", {
_validate: function() { _validate: function() {
if (this.quote != null) { if (this.quote != null) {
if (typeof this.quote != "string") throw new Error("quote must be string"); 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"); if (typeof this.value != "string") throw new Error("value must be string");
}, },

View File

@@ -173,6 +173,7 @@ Compressor.prototype = new TreeTransformer;
merge(Compressor.prototype, { merge(Compressor.prototype, {
option: function(key) { return this.options[key] }, option: function(key) { return this.options[key] },
exposed: function(def) { exposed: function(def) {
if (def.exported) return true;
if (def.undeclared) return true; if (def.undeclared) return true;
if (!(def.global || def.scope.resolve() instanceof AST_Toplevel)) return false; if (!(def.global || def.scope.resolve() instanceof AST_Toplevel)) return false;
var toplevel = this.toplevel; var toplevel = this.toplevel;
@@ -5583,7 +5584,7 @@ merge(Compressor.prototype, {
if (scope === self) { if (scope === self) {
if (node instanceof AST_LambdaDefinition) { if (node instanceof AST_LambdaDefinition) {
var def = node.name.definition(); 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_ids[def.id] = true;
in_use.push(def); in_use.push(def);
} }
@@ -5602,7 +5603,7 @@ merge(Compressor.prototype, {
var redef = def.redefined(); var redef = def.redefined();
if (redef) var_defs[redef.id] = (var_defs[redef.id] || 0) + 1; 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_Const ? def.redefined() : def.const_redefs)
|| !(node instanceof AST_Var || is_safe_lexical(def)))) { || !(node instanceof AST_Var || is_safe_lexical(def)))) {
in_use_ids[def.id] = true; in_use_ids[def.id] = true;

View File

@@ -892,10 +892,6 @@ function OutputStream(options) {
use_asm = was_asm; use_asm = was_asm;
} }
DEFPRINT(AST_Statement, function(output) {
this.body.print(output);
output.semicolon();
});
DEFPRINT(AST_Toplevel, function(output) { DEFPRINT(AST_Toplevel, function(output) {
display_body(this.body, true, output, true); display_body(this.body, true, output, true);
output.print(""); output.print("");
@@ -1011,6 +1007,64 @@ function OutputStream(options) {
output.space(); output.space();
force_statement(self.body, output); 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) { DEFPRINT(AST_Import, function(output) {
var self = this; var self = this;
output.print("import"); output.print("import");
@@ -1543,6 +1597,16 @@ function OutputStream(options) {
DEFPRINT(AST_Symbol, function(output) { DEFPRINT(AST_Symbol, function(output) {
print_symbol(this, 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) { DEFPRINT(AST_SymbolImport, function(output) {
var self = this; var self = this;
if (self.key) { if (self.key) {

View File

@@ -844,6 +844,9 @@ function parse($TEXT, options) {
case "await": case "await":
if (S.in_async) return simple_statement(); if (S.in_async) return simple_statement();
break; break;
case "export":
next();
return export_();
case "import": case "import":
next(); next();
return import_(); 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_() { function import_() {
var all = null; var all = null;
var def = as_symbol(AST_SymbolImport, true); var def = as_symbol(AST_SymbolImport, true);
@@ -1288,7 +1400,7 @@ function parse($TEXT, options) {
} else { } else {
expect("{"); expect("{");
props = []; props = [];
while (is("name") || is_identifier_string(S.token.value)) { while (is_alias()) {
var alias; var alias;
if (is_token(peek(), "name", "as")) { if (is_token(peek(), "name", "as")) {
var key = S.token.value; var key = S.token.value;
@@ -1307,9 +1419,8 @@ function parse($TEXT, options) {
} }
} }
if (all || def || props) expect_token("name", "from"); if (all || def || props) expect_token("name", "from");
if (!is("string")) unexpected();
var path = S.token; var path = S.token;
next(); expect_token("string");
semicolon(); semicolon();
return new AST_Import({ return new AST_Import({
all: all, all: all,

View File

@@ -45,6 +45,7 @@
function SymbolDef(id, scope, orig, init) { function SymbolDef(id, scope, orig, init) {
this.eliminated = 0; this.eliminated = 0;
this.exported = false;
this.global = false; this.global = false;
this.id = id; this.id = id;
this.init = init; this.init = init;
@@ -91,6 +92,7 @@ SymbolDef.prototype = {
}, },
unmangleable: function(options) { unmangleable: function(options) {
return this.global && !options.toplevel return this.global && !options.toplevel
|| this.exported
|| this.undeclared || this.undeclared
|| !options.eval && this.scope.pinned() || !options.eval && this.scope.pinned()
|| options.keep_fnames || options.keep_fnames
@@ -118,11 +120,22 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options) {
// pass 1: setup scope chaining and handle definitions // pass 1: setup scope chaining and handle definitions
var self = this; var self = this;
var defun = null; var defun = null;
var exported = false;
var next_def_id = 0; var next_def_id = 0;
var scope = self.parent_scope = null; var scope = self.parent_scope = null;
var tw = new TreeWalker(function(node, descend) { 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) { if (node instanceof AST_LambdaDefinition) {
var save_exported = exported;
exported = tw.parent() instanceof AST_ExportDeclaration;
node.name.walk(tw); node.name.walk(tw);
exported = save_exported;
walk_scope(function() { walk_scope(function() {
node.argnames.forEach(function(argname) { node.argnames.forEach(function(argname) {
argname.walk(tw); argname.walk(tw);
@@ -169,9 +182,11 @@ AST_Toplevel.DEFMETHOD("figure_out_scope", function(options) {
if (node instanceof AST_SymbolCatch) { if (node instanceof AST_SymbolCatch) {
scope.def_variable(node).defun = defun; scope.def_variable(node).defun = defun;
} else if (node instanceof AST_SymbolConst) { } 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) { } else if (node instanceof AST_SymbolDefun) {
defun.def_function(node, tw.parent()); defun.def_function(node, tw.parent()).exported = exported;
entangle(defun, scope); entangle(defun, scope);
} else if (node instanceof AST_SymbolFunarg) { } else if (node instanceof AST_SymbolFunarg) {
defun.def_variable(node); 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); var def = defun.def_function(node, node.name == "arguments" ? undefined : defun);
if (options.ie8) def.defun = defun.parent_scope.resolve(); if (options.ie8) def.defun = defun.parent_scope.resolve();
} else if (node instanceof AST_SymbolLet) { } else if (node instanceof AST_SymbolLet) {
scope.def_variable(node); scope.def_variable(node).exported = exported;
} else if (node instanceof AST_SymbolVar) { } else if (node instanceof AST_SymbolVar) {
defun.def_variable(node, null); defun.def_variable(node, null).exported = exported;
entangle(defun, scope); entangle(defun, scope);
} }

View File

@@ -204,6 +204,15 @@ TreeTransformer.prototype = new TreeWalker;
if (self.key instanceof AST_Node) self.key = self.key.transform(tw); if (self.key instanceof AST_Node) self.key = self.key.transform(tw);
self.value = self.value.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) { DEF(AST_Import, function(self, tw) {
if (self.all) self.all = self.all.transform(tw); if (self.all) self.all = self.all.transform(tw);
if (self.default) self.default = self.default.transform(tw); if (self.default) self.default = self.default.transform(tw);

View File

@@ -249,12 +249,14 @@ function first_in_statement(stack, arrow) {
if (p.expression === node) continue; if (p.expression === node) continue;
} else if (p instanceof AST_Conditional) { } else if (p instanceof AST_Conditional) {
if (p.condition === node) continue; if (p.condition === node) continue;
} else if (p instanceof AST_ExportDefault) {
return false;
} else if (p instanceof AST_PropAccess) { } else if (p instanceof AST_PropAccess) {
if (p.expression === node) continue; if (p.expression === node) continue;
} else if (p instanceof AST_Sequence) { } else if (p instanceof AST_Sequence) {
if (p.expressions[0] === node) continue; if (p.expressions[0] === node) continue;
} else if (p instanceof AST_Statement) { } else if (p instanceof AST_SimpleStatement) {
return p.body === node; return true;
} else if (p instanceof AST_Template) { } else if (p instanceof AST_Template) {
if (p.tag === node) continue; if (p.tag === node) continue;
} else if (p instanceof AST_UnaryPostfix) { } else if (p instanceof AST_UnaryPostfix) {

144
test/compress/exports.js Normal file
View File

@@ -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);
}
}
}

71
test/mocha/exports.js Normal file
View File

@@ -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);
});
});
});