support import statements (#4646)

This commit is contained in:
Alex Lam S.L
2021-02-13 20:26:43 +00:00
committed by GitHub
parent a6bb66931b
commit b7219ac489
8 changed files with 312 additions and 10 deletions

View File

@@ -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 - `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: - `inline` (default: `true`) -- inline calls to function with simple/`return` statement:
- `false` -- same as `0` - `false` -- same as `0`
- `0` -- disabled inlining - `0` -- disabled inlining

View File

@@ -198,13 +198,16 @@ var AST_Debugger = DEFNODE("Debugger", null, {
$documentation: "Represents a debugger statement", $documentation: "Represents a debugger statement",
}, AST_Statement); }, AST_Statement);
var AST_Directive = DEFNODE("Directive", "value quote", { var AST_Directive = DEFNODE("Directive", "quote value", {
$documentation: "Represents a directive, like \"use strict\";", $documentation: "Represents a directive, like \"use strict\";",
$propdoc: { $propdoc: {
quote: "[string?] the original quote character",
value: "[string] The value of this directive as a plain string (it's not an AST_String!)", 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() { _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"); if (typeof this.value != "string") throw new Error("value must be string");
}, },
}, AST_Statement); }, AST_Statement);
@@ -1035,6 +1038,44 @@ var AST_VarDef = DEFNODE("VarDef", "name value", {
/* -----[ OTHER ]----- */ /* -----[ 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", { var AST_DefaultValue = DEFNODE("DefaultValue", "name value", {
$documentation: "A default value declaration", $documentation: "A default value declaration",
$propdoc: { $propdoc: {
@@ -1494,6 +1535,16 @@ var AST_SymbolFunarg = DEFNODE("SymbolFunarg", null, {
$documentation: "Symbol naming a function argument", $documentation: "Symbol naming a function argument",
}, AST_SymbolVar); }, 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, { var AST_SymbolDefun = DEFNODE("SymbolDefun", null, {
$documentation: "Symbol defining a function", $documentation: "Symbol defining a function",
}, AST_SymbolDeclaration); }, 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", $documentation: "A string literal",
$propdoc: { $propdoc: {
quote: "[string?] the original quote character",
value: "[string] the contents of this string", value: "[string] the contents of this string",
quote: "[string] the original quote character"
}, },
_validate: function() { _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"); if (typeof this.value != "string") throw new Error("value must be string");
}, },
}, AST_Constant); }, AST_Constant);

View File

@@ -70,6 +70,7 @@ function Compressor(options, false_by_default) {
hoist_vars : false, hoist_vars : false,
ie8 : false, ie8 : false,
if_return : !false_by_default, if_return : !false_by_default,
imports : !false_by_default,
inline : !false_by_default, inline : !false_by_default,
join_vars : !false_by_default, join_vars : !false_by_default,
keep_fargs : false_by_default, keep_fargs : false_by_default,
@@ -6081,6 +6082,10 @@ merge(Compressor.prototype, {
scope = save_scope; scope = save_scope;
return node; 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) { }, function(node, in_list) {
if (node instanceof AST_BlockStatement) { if (node instanceof AST_BlockStatement) {
return trim_block(node, in_list); return trim_block(node, in_list);

View File

@@ -1011,6 +1011,27 @@ function OutputStream(options) {
output.space(); output.space();
force_statement(self.body, output); 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 ]----- */ /* -----[ functions ]----- */
function print_funargs(self, output) { function print_funargs(self, output) {
@@ -1454,8 +1475,8 @@ function OutputStream(options) {
}); });
else print_braced_empty(this, output); else print_braced_empty(this, output);
}); });
DEFPRINT(AST_Object, function(output) { function print_properties(self, output) {
var props = this.properties; var props = self.properties;
if (props.length > 0) output.with_block(function() { if (props.length > 0) output.with_block(function() {
props.forEach(function(prop, i) { props.forEach(function(prop, i) {
if (i) { if (i) {
@@ -1467,7 +1488,10 @@ function OutputStream(options) {
}); });
output.newline(); 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) { function print_property_key(self, output) {
@@ -1512,9 +1536,22 @@ function OutputStream(options) {
} }
DEFPRINT(AST_ObjectGetter, print_accessor("get")); DEFPRINT(AST_ObjectGetter, print_accessor("get"));
DEFPRINT(AST_ObjectSetter, print_accessor("set")); 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) { DEFPRINT(AST_Symbol, function(output) {
var def = this.definition(); print_symbol(this, output);
output.print_name(def && def.mangled_name || this.name); });
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_Hole, noop);
DEFPRINT(AST_This, function(output) { DEFPRINT(AST_This, function(output) {

View File

@@ -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 = "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 KEYWORDS_ATOM = "false null true";
var RESERVED_WORDS = [ 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_ATOM,
KEYWORDS, KEYWORDS,
].join(" "); ].join(" ");
@@ -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 "import":
next();
return import_();
case "yield": case "yield":
if (S.in_generator) return simple_statement(); if (S.in_generator) return simple_statement();
break; 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_() { function block_() {
expect("{"); expect("{");
var a = []; var a = [];

View File

@@ -204,6 +204,11 @@ 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_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) { DEF(AST_Template, function(self, tw) {
if (self.tag) self.tag = self.tag.transform(tw); if (self.tag) self.tag = self.tag.transform(tw);
self.expressions = do_list(self.expressions, tw); self.expressions = do_list(self.expressions, tw);

123
test/compress/imports.js Normal file
View File

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

28
test/mocha/imports.js Normal file
View File

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