aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Lam S.L <alexlamsl@gmail.com>2021-02-14 20:13:54 +0000
committerGitHub <noreply@github.com>2021-02-15 04:13:54 +0800
commitc21f096ab882dc37771375c4eadafe61a3e6ac51 (patch)
treee3749eac5e571015c939947735736e148f5ac1eb
parentb7219ac489e47091f17091a08d7ef50980d68972 (diff)
downloadtracifyjs-c21f096ab882dc37771375c4eadafe61a3e6ac51.tar.gz
tracifyjs-c21f096ab882dc37771375c4eadafe61a3e6ac51.zip
support `export` statements (#4650)
-rw-r--r--lib/ast.js95
-rw-r--r--lib/compress.js5
-rw-r--r--lib/output.js72
-rw-r--r--lib/parse.js117
-rw-r--r--lib/scope.js23
-rw-r--r--lib/transform.js9
-rw-r--r--lib/utils.js6
-rw-r--r--test/compress/exports.js144
-rw-r--r--test/mocha/exports.js71
9 files changed, 526 insertions, 16 deletions
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);
+ });
+ });
+});