aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Lam S.L <alexlamsl@gmail.com>2021-02-13 20:26:43 +0000
committerGitHub <noreply@github.com>2021-02-14 04:26:43 +0800
commitb7219ac489e47091f17091a08d7ef50980d68972 (patch)
treec46da154cf918ec3573ddb9b4dfd9df244919556
parenta6bb66931bfdd04d6ce05b640d89a8deff7ca3de (diff)
downloadtracifyjs-b7219ac489e47091f17091a08d7ef50980d68972.tar.gz
tracifyjs-b7219ac489e47091f17091a08d7ef50980d68972.zip
support `import` statements (#4646)
-rw-r--r--README.md2
-rw-r--r--lib/ast.js62
-rw-r--r--lib/compress.js5
-rw-r--r--lib/output.js47
-rw-r--r--lib/parse.js50
-rw-r--r--lib/transform.js5
-rw-r--r--test/compress/imports.js123
-rw-r--r--test/mocha/imports.js28
8 files changed, 312 insertions, 10 deletions
diff --git a/README.md b/README.md
index e3d1d677..09da3aeb 100644
--- a/README.md
+++ b/README.md
@@ -685,6 +685,8 @@ to be `false` and all symbol names will be omitted.
- `if_return` (default: `true`) -- optimizations for if/return and if/continue
+- `imports` (default: `true`) -- drop unreferenced import symbols when used with `unused`
+
- `inline` (default: `true`) -- inline calls to function with simple/`return` statement:
- `false` -- same as `0`
- `0` -- disabled inlining
diff --git a/lib/ast.js b/lib/ast.js
index 7c6fcaf0..24b5a875 100644
--- a/lib/ast.js
+++ b/lib/ast.js
@@ -198,13 +198,16 @@ var AST_Debugger = DEFNODE("Debugger", null, {
$documentation: "Represents a debugger statement",
}, AST_Statement);
-var AST_Directive = DEFNODE("Directive", "value quote", {
+var AST_Directive = DEFNODE("Directive", "quote value", {
$documentation: "Represents a directive, like \"use strict\";",
$propdoc: {
+ quote: "[string?] the original quote character",
value: "[string] The value of this directive as a plain string (it's not an AST_String!)",
- quote: "[string] the original quote character"
},
_validate: function() {
+ if (this.quote != null) {
+ if (typeof this.quote != "string") throw new Error("quote must be string");
+ }
if (typeof this.value != "string") throw new Error("value must be string");
},
}, AST_Statement);
@@ -1035,6 +1038,44 @@ var AST_VarDef = DEFNODE("VarDef", "name value", {
/* -----[ OTHER ]----- */
+var AST_Import = DEFNODE("Import", "all default path properties quote", {
+ $documentation: "An `import` statement",
+ $propdoc: {
+ all: "[AST_SymbolImport?] the imported namespace, or null if not specified",
+ default: "[AST_SymbolImport?] the alias for default `export`, or null if not specified",
+ path: "[string] the path to import module",
+ properties: "[(AST_SymbolImport*)?] array of aliases, or null if not specified",
+ quote: "[string?] the original quote character",
+ },
+ walk: function(visitor) {
+ var node = this;
+ visitor.visit(node, function() {
+ if (node.all) node.all.walk(visitor);
+ if (node.default) node.default.walk(visitor);
+ if (node.properties) node.properties.forEach(function(prop) {
+ prop.walk(visitor);
+ });
+ });
+ },
+ _validate: function() {
+ if (this.all != null) {
+ if (!(this.all instanceof AST_SymbolImport)) throw new Error("all must be AST_SymbolImport");
+ if (this.properties != null) throw new Error("cannot import both * and {} in the same statement");
+ }
+ if (this.default != null) {
+ if (!(this.default instanceof AST_SymbolImport)) throw new Error("default must be AST_SymbolImport");
+ if (this.default.key !== "") throw new Error("invalid default key: " + this.default.key);
+ }
+ if (typeof this.path != "string") throw new Error("path must be string");
+ if (this.properties != null) this.properties.forEach(function(node) {
+ if (!(node instanceof AST_SymbolImport)) throw new Error("properties must contain AST_SymbolImport");
+ });
+ if (this.quote != null) {
+ if (typeof this.quote != "string") throw new Error("quote must be string");
+ }
+ },
+}, AST_Statement);
+
var AST_DefaultValue = DEFNODE("DefaultValue", "name value", {
$documentation: "A default value declaration",
$propdoc: {
@@ -1494,6 +1535,16 @@ var AST_SymbolFunarg = DEFNODE("SymbolFunarg", null, {
$documentation: "Symbol naming a function argument",
}, AST_SymbolVar);
+var AST_SymbolImport = DEFNODE("SymbolImport", "key", {
+ $documentation: "Symbol defined by an `import` statement",
+ $propdoc: {
+ key: "[string] the original `export` name",
+ },
+ _validate: function() {
+ if (typeof this.key != "string") throw new Error("key must be string");
+ },
+}, AST_SymbolVar);
+
var AST_SymbolDefun = DEFNODE("SymbolDefun", null, {
$documentation: "Symbol defining a function",
}, AST_SymbolDeclaration);
@@ -1567,13 +1618,16 @@ var AST_Constant = DEFNODE("Constant", null, {
},
});
-var AST_String = DEFNODE("String", "value quote", {
+var AST_String = DEFNODE("String", "quote value", {
$documentation: "A string literal",
$propdoc: {
+ quote: "[string?] the original quote character",
value: "[string] the contents of this string",
- quote: "[string] the original quote character"
},
_validate: function() {
+ if (this.quote != null) {
+ if (typeof this.quote != "string") throw new Error("quote must be string");
+ }
if (typeof this.value != "string") throw new Error("value must be string");
},
}, AST_Constant);
diff --git a/lib/compress.js b/lib/compress.js
index 7e113ecc..f7151441 100644
--- a/lib/compress.js
+++ b/lib/compress.js
@@ -70,6 +70,7 @@ function Compressor(options, false_by_default) {
hoist_vars : false,
ie8 : false,
if_return : !false_by_default,
+ imports : !false_by_default,
inline : !false_by_default,
join_vars : !false_by_default,
keep_fargs : false_by_default,
@@ -6081,6 +6082,10 @@ merge(Compressor.prototype, {
scope = save_scope;
return node;
}
+ if (node instanceof AST_SymbolImport) {
+ if (!compressor.option("imports") || node.definition().id in in_use_ids) return node;
+ return in_list ? List.skip : null;
+ }
}, function(node, in_list) {
if (node instanceof AST_BlockStatement) {
return trim_block(node, in_list);
diff --git a/lib/output.js b/lib/output.js
index 3406daa1..150f270a 100644
--- a/lib/output.js
+++ b/lib/output.js
@@ -1011,6 +1011,27 @@ function OutputStream(options) {
output.space();
force_statement(self.body, output);
});
+ DEFPRINT(AST_Import, function(output) {
+ var self = this;
+ output.print("import");
+ output.space();
+ if (self.default) self.default.print(output);
+ if (self.all) {
+ if (self.default) output.comma();
+ self.all.print(output);
+ }
+ if (self.properties) {
+ if (self.default) output.comma();
+ print_properties(self, output);
+ }
+ if (self.all || self.default || self.properties) {
+ output.space();
+ output.print("from");
+ output.space();
+ }
+ output.print_string(self.path, self.quote);
+ output.semicolon();
+ });
/* -----[ functions ]----- */
function print_funargs(self, output) {
@@ -1454,8 +1475,8 @@ function OutputStream(options) {
});
else print_braced_empty(this, output);
});
- DEFPRINT(AST_Object, function(output) {
- var props = this.properties;
+ function print_properties(self, output) {
+ var props = self.properties;
if (props.length > 0) output.with_block(function() {
props.forEach(function(prop, i) {
if (i) {
@@ -1467,7 +1488,10 @@ function OutputStream(options) {
});
output.newline();
});
- else print_braced_empty(this, output);
+ else print_braced_empty(self, output);
+ }
+ DEFPRINT(AST_Object, function(output) {
+ print_properties(this, output);
});
function print_property_key(self, output) {
@@ -1512,9 +1536,22 @@ function OutputStream(options) {
}
DEFPRINT(AST_ObjectGetter, print_accessor("get"));
DEFPRINT(AST_ObjectSetter, print_accessor("set"));
+ function print_symbol(self, output) {
+ var def = self.definition();
+ output.print_name(def && def.mangled_name || self.name);
+ }
DEFPRINT(AST_Symbol, function(output) {
- var def = this.definition();
- output.print_name(def && def.mangled_name || this.name);
+ print_symbol(this, output);
+ });
+ DEFPRINT(AST_SymbolImport, function(output) {
+ var self = this;
+ if (self.key) {
+ output.print_name(self.key);
+ output.space();
+ output.print("as");
+ output.space();
+ }
+ print_symbol(self, output);
});
DEFPRINT(AST_Hole, noop);
DEFPRINT(AST_This, function(output) {
diff --git a/lib/parse.js b/lib/parse.js
index 27803f2e..e287c40e 100644
--- a/lib/parse.js
+++ b/lib/parse.js
@@ -47,7 +47,7 @@
var KEYWORDS = "break case catch const continue debugger default delete do else finally for function if in instanceof let new return switch throw try typeof var void while with";
var KEYWORDS_ATOM = "false null true";
var RESERVED_WORDS = [
- "await abstract boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized this throws transient volatile yield",
+ "abstract async await boolean byte char class double enum export extends final float goto implements import int interface long native package private protected public short static super synchronized this throws transient volatile yield",
KEYWORDS_ATOM,
KEYWORDS,
].join(" ");
@@ -844,6 +844,9 @@ function parse($TEXT, options) {
case "await":
if (S.in_async) return simple_statement();
break;
+ case "import":
+ next();
+ return import_();
case "yield":
if (S.in_generator) return simple_statement();
break;
@@ -1272,6 +1275,51 @@ function parse($TEXT, options) {
});
}
+ function import_() {
+ var all = null;
+ var def = as_symbol(AST_SymbolImport, true);
+ var props = null;
+ if (def ? (def.key = "", is("punc", ",") && next()) : !is("string")) {
+ if (is("operator", "*")) {
+ next();
+ expect_token("name", "as");
+ all = as_symbol(AST_SymbolImport);
+ all.key = "*";
+ } else {
+ expect("{");
+ props = [];
+ while (is("name") || is_identifier_string(S.token.value)) {
+ var alias;
+ if (is_token(peek(), "name", "as")) {
+ var key = S.token.value;
+ next();
+ next();
+ alias = as_symbol(AST_SymbolImport);
+ alias.key = key;
+ } else {
+ alias = as_symbol(AST_SymbolImport);
+ alias.key = alias.name;
+ }
+ props.push(alias);
+ if (!is("punc", "}")) expect(",");
+ }
+ expect("}");
+ }
+ }
+ if (all || def || props) expect_token("name", "from");
+ if (!is("string")) unexpected();
+ var path = S.token;
+ next();
+ semicolon();
+ return new AST_Import({
+ all: all,
+ default: def,
+ path: path.value,
+ properties: props,
+ quote: path.quote,
+ });
+ }
+
function block_() {
expect("{");
var a = [];
diff --git a/lib/transform.js b/lib/transform.js
index 716fb148..b6a0295a 100644
--- a/lib/transform.js
+++ b/lib/transform.js
@@ -204,6 +204,11 @@ TreeTransformer.prototype = new TreeWalker;
if (self.key instanceof AST_Node) self.key = self.key.transform(tw);
self.value = self.value.transform(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);
+ if (self.properties) self.properties = do_list(self.properties, tw);
+ });
DEF(AST_Template, function(self, tw) {
if (self.tag) self.tag = self.tag.transform(tw);
self.expressions = do_list(self.expressions, tw);
diff --git a/test/compress/imports.js b/test/compress/imports.js
new file mode 100644
index 00000000..cdc239cd
--- /dev/null
+++ b/test/compress/imports.js
@@ -0,0 +1,123 @@
+nought: {
+ input: {
+ import "foo";
+ }
+ expect_exact: 'import"foo";'
+}
+
+default_only: {
+ input: {
+ import foo from "bar";
+ }
+ expect_exact: 'import foo from"bar";'
+}
+
+all_only: {
+ input: {
+ import * as foo from "bar";
+ }
+ expect_exact: 'import*as foo from"bar";'
+}
+
+keys_only: {
+ input: {
+ import { as as foo, bar, delete as baz } from "moo";
+ }
+ expect_exact: 'import{as as foo,bar as bar,delete as baz}from"moo";'
+}
+
+default_all: {
+ input: {
+ import foo, * as bar from "baz";
+ }
+ expect_exact: 'import foo,*as bar from"baz";'
+}
+
+default_keys: {
+ input: {
+ import foo, { bar } from "baz";
+ }
+ expect_exact: 'import foo,{bar as bar}from"baz";'
+}
+
+dynamic: {
+ input: {
+ (async a => await import(a))("foo").then(bar);
+ }
+ expect_exact: '(async a=>await import(a))("foo").then(bar);'
+}
+
+import_meta: {
+ input: {
+ console.log(import.meta, import.meta.url);
+ }
+ expect_exact: "console.log(import.meta,import.meta.url);"
+}
+
+same_quotes: {
+ beautify = {
+ beautify: true,
+ quote_style: 3,
+ }
+ input: {
+ import 'foo';
+ import "bar";
+ }
+ expect_exact: [
+ "import 'foo';",
+ "",
+ 'import "bar";',
+ ]
+}
+
+drop_unused: {
+ options = {
+ imports: true,
+ toplevel: true,
+ unused: true,
+ }
+ input: {
+ import a, * as b from "foo";
+ import { c, bar as d } from "baz";
+ console.log(c);
+ }
+ expect: {
+ import "foo";
+ import { c as c } from "baz";
+ console.log(c);
+ }
+}
+
+mangle: {
+ rename = false
+ mangle = {
+ toplevel: true,
+ }
+ input: {
+ import foo, { bar } from "baz";
+ consoe.log(moo);
+ import * as moo from "moz";
+ }
+ expect: {
+ import o, { bar as m } from "baz";
+ consoe.log(r);
+ import * as r from "moz";
+ }
+}
+
+rename_mangle: {
+ rename = true
+ mangle = {
+ toplevel: true,
+ }
+ input: {
+ import foo, { bar } from "baz";
+ consoe.log(moo);
+ import * as moo from "moz";
+ }
+ expect: {
+ import o, { bar as m } from "baz";
+ consoe.log(r);
+ import * as r from "moz";
+ }
+}
diff --git a/test/mocha/imports.js b/test/mocha/imports.js
new file mode 100644
index 00000000..e9b654cd
--- /dev/null
+++ b/test/mocha/imports.js
@@ -0,0 +1,28 @@
+var assert = require("assert");
+var UglifyJS = require("../node");
+
+describe("import", function() {
+ it("Should reject invalid `import` statement syntax", function() {
+ [
+ "import *;",
+ "import A;",
+ "import {};",
+ "import `path`;",
+ "import from 'path';",
+ "import * from 'path';",
+ "import A as B from 'path';",
+ "import { A }, B from 'path';",
+ "import * as A, B from 'path';",
+ "import * as A, {} from 'path';",
+ "import { * as A } from 'path';",
+ "import { 42 as A } from 'path';",
+ "import { 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);
+ });
+ });
+});