support export statements properly (#2126)

- mangle & compress correctly with `toplevel`
- retain non-toplevel import/export
- parse & output `export { variable as name }`
- remove extraneous spaces from `beautify`


fixes #2038
fixes #2124
This commit is contained in:
Alex Lam S.L
2017-06-21 00:51:36 +08:00
committed by GitHub
parent 849ba79dee
commit 62d1fbf645
7 changed files with 199 additions and 57 deletions

View File

@@ -629,11 +629,11 @@ var AST_Const = DEFNODE("Const", null, {
$documentation: "A `const` statement"
}, AST_Definitions);
var AST_NameImport = DEFNODE("NameImport", "foreign_name name", {
$documentation: "The part of the import statement that imports names from a module.",
var AST_NameMapping = DEFNODE("NameMapping", "foreign_name name", {
$documentation: "The part of the export/import statement that declare names from a module.",
$propdoc: {
foreign_name: "[AST_SymbolImportForeign] The name being imported (as specified in the module)",
name: "[AST_SymbolImport] The name as it becomes available to this module."
foreign_name: "[AST_SymbolExportForeign|AST_SymbolImportForeign] The name being exported/imported (as specified in the module)",
name: "[AST_SymbolExport|AST_SymbolImport] The name as it is visible to this module."
},
_walk: function (visitor) {
return visitor._visit(this, function() {
@@ -647,7 +647,7 @@ var AST_Import = DEFNODE("Import", "imported_name imported_names module_name", {
$documentation: "An `import` statement",
$propdoc: {
imported_name: "[AST_SymbolImport] The name of the variable holding the module's default export.",
imported_names: "[AST_NameImport*] The names of non-default imported variables",
imported_names: "[AST_NameMapping*] The names of non-default imported variables",
module_name: "[AST_String] String literal describing where this module came from",
},
_walk: function(visitor) {
@@ -656,7 +656,7 @@ var AST_Import = DEFNODE("Import", "imported_name imported_names module_name", {
this.imported_name._walk(visitor);
}
if (this.imported_names) {
this.imported_names.forEach(function (name_import) {
this.imported_names.forEach(function(name_import) {
name_import._walk(visitor);
});
}
@@ -670,7 +670,7 @@ var AST_Export = DEFNODE("Export", "exported_definition exported_value is_defaul
$propdoc: {
exported_definition: "[AST_Defun|AST_Definitions|AST_DefClass?] An exported definition",
exported_value: "[AST_Node?] An exported value",
exported_names: "[AST_NameImport*?] List of exported names",
exported_names: "[AST_NameMapping*?] List of exported names",
module_name: "[AST_String?] Name of the file to load exports from",
is_default: "[Boolean] Whether this is the default exported value of this module"
},
@@ -682,6 +682,14 @@ var AST_Export = DEFNODE("Export", "exported_definition exported_value is_defaul
if (this.exported_value) {
this.exported_value._walk(visitor);
}
if (this.exported_names) {
this.exported_names.forEach(function(name_export) {
name_export._walk(visitor);
});
}
if (this.module_name) {
this.module_name._walk(visitor);
}
});
}
}, AST_Statement);
@@ -996,7 +1004,7 @@ var AST_SymbolCatch = DEFNODE("SymbolCatch", null, {
}, AST_SymbolBlockDeclaration);
var AST_SymbolImport = DEFNODE("SymbolImport", null, {
$documentation: "Symbol refering to an imported name",
$documentation: "Symbol referring to an imported name",
}, AST_SymbolBlockDeclaration);
var AST_SymbolImportForeign = DEFNODE("SymbolImportForeign", null, {
@@ -1018,6 +1026,14 @@ var AST_SymbolRef = DEFNODE("SymbolRef", null, {
$documentation: "Reference to some symbol (not definition/declaration)",
}, AST_Symbol);
var AST_SymbolExport = DEFNODE("SymbolExport", null, {
$documentation: "Symbol referring to a name to export",
}, AST_SymbolRef);
var AST_SymbolExportForeign = DEFNODE("SymbolExportForeign", null, {
$documentation: "A symbol exported from this module, but it is used in the other module, and its real name is irrelevant for this module's purposes",
}, AST_Symbol);
var AST_LabelRef = DEFNODE("LabelRef", null, {
$documentation: "Reference to a label symbol",
}, AST_Symbol);

View File

@@ -672,7 +672,9 @@ merge(Compressor.prototype, {
node instanceof AST_DefClass ||
node instanceof AST_Defun ||
node instanceof AST_Let ||
node instanceof AST_Const
node instanceof AST_Const ||
node instanceof AST_Export ||
node instanceof AST_Import
);
}
@@ -2099,15 +2101,16 @@ merge(Compressor.prototype, {
return true; // don't go in nested scopes
}
if (node instanceof AST_Definitions && scope === self) {
var in_export = tw.parent() instanceof AST_Export;
node.definitions.forEach(function(def){
if (def.name instanceof AST_SymbolVar) {
var_defs_by_id.add(def.name.definition().id, def);
}
if (!drop_vars) {
if (in_export || !drop_vars) {
def.name.walk(new TreeWalker(function(node) {
if (node instanceof AST_SymbolDeclaration) {
var def = node.definition();
if (def.global && !(def.id in in_use_ids)) {
if ((in_export || def.global) && !(def.id in in_use_ids)) {
in_use_ids[def.id] = true;
in_use.push(def);
}
@@ -4037,6 +4040,10 @@ merge(Compressor.prototype, {
return self;
});
OPT(AST_SymbolExport, function(self, compressor){
return self;
});
OPT(AST_SymbolRef, function(self, compressor){
var def = self.resolve_defines(compressor);
if (def) {

View File

@@ -1280,7 +1280,6 @@ function OutputStream(options) {
name_import.print(output);
if (i < self.imported_names.length - 1) {
output.print(",");
output.space();
}
});
output.space();
@@ -1296,17 +1295,26 @@ function OutputStream(options) {
output.semicolon();
});
DEFPRINT(AST_NameImport, function(self, output) {
DEFPRINT(AST_NameMapping, function(self, output) {
var is_import = output.parent() instanceof AST_Import;
var definition = self.name.definition();
var names_are_different =
(definition && definition.mangled_name || self.name.name) !==
self.foreign_name.name;
if (names_are_different) {
output.print(self.foreign_name.name);
if (is_import) {
output.print(self.foreign_name.name);
} else {
self.name.print(output);
}
output.space();
output.print("as");
output.space();
self.name.print(output);
if (is_import) {
self.name.print(output);
} else {
output.print(self.foreign_name.name);
}
} else {
self.name.print(output);
}
@@ -1320,24 +1328,20 @@ function OutputStream(options) {
output.space();
}
if (self.exported_names) {
output.space();
if (self.exported_names.length === 1 && self.exported_names[0].name.name === "*") {
self.exported_names[0].print(output);
} else {
output.print("{");
self.exported_names.forEach(function (name_import, i) {
self.exported_names.forEach(function(name_export, i) {
output.space();
name_import.print(output);
name_export.print(output);
if (i < self.exported_names.length - 1) {
output.print(",");
output.space();
}
});
output.space();
output.print("}");
}
output.space();
}
else if (self.exported_value) {
self.exported_value.print(output);

View File

@@ -2302,7 +2302,7 @@ function parse($TEXT, options) {
next();
}
imported_names = import_names(true);
imported_names = map_names(true);
if (imported_names || imported_name) {
expect_token("name", "from");
@@ -2326,26 +2326,32 @@ function parse($TEXT, options) {
});
}
function import_name() {
function map_name(is_import) {
var foreign_type = is_import ? AST_SymbolImportForeign : AST_SymbolExportForeign;
var type = is_import ? AST_SymbolImport : AST_SymbolExport;
var start = S.token;
var foreign_name;
var name;
if (peek().value === "as" && peek().type === "name") {
foreign_name = as_symbol(AST_SymbolImportForeign);
if (is_import) {
foreign_name = as_symbol(foreign_type);
} else {
name = as_symbol(type);
}
if (is("name", "as")) {
next(); // The "as" word
}
name = as_symbol(AST_SymbolImport);
if (foreign_name === undefined) {
foreign_name = new AST_SymbolImportForeign({
name: name.name,
start: name.start,
end: name.end,
});
if (is_import) {
name = as_symbol(type);
} else {
foreign_name = as_symbol(foreign_type);
}
} else if (is_import) {
name = new type(foreign_name);
} else {
foreign_name = new foreign_type(name);
}
return new AST_NameImport({
return new AST_NameMapping({
start: start,
foreign_name: foreign_name,
name: name,
@@ -2353,26 +2359,26 @@ function parse($TEXT, options) {
})
}
function import_nameAsterisk(name) {
function map_nameAsterisk(is_import, name) {
var foreign_type = is_import ? AST_SymbolImportForeign : AST_SymbolExportForeign;
var type = is_import ? AST_SymbolImport : AST_SymbolExport;
var start = S.token;
var foreign_name;
var end = prev();
name = name || new AST_SymbolImport({
name = name || new type({
name: '*',
start: start,
end: end,
});
foreign_name = new AST_SymbolImportForeign({
foreign_name = new foreign_type({
name: '*',
start: start,
end: end,
});
return new AST_NameImport({
return new AST_NameMapping({
start: start,
foreign_name: foreign_name,
name: name,
@@ -2380,13 +2386,13 @@ function parse($TEXT, options) {
})
}
function import_names(allow_as) {
function map_names(is_import) {
var names;
if (is("punc", "{")) {
next();
names = [];
while (!is("punc", "}")) {
names.push(import_name());
names.push(map_name(is_import));
if (is("punc", ",")) {
next();
}
@@ -2395,11 +2401,11 @@ function parse($TEXT, options) {
} else if (is("operator", "*")) {
var name;
next();
if (allow_as && is("name", "as")) {
if (is_import && is("name", "as")) {
next(); // The "as" word
name = as_symbol(AST_SymbolImportForeign);
}
names = [import_nameAsterisk(name)];
names = [map_nameAsterisk(is_import, name)];
}
return names;
}
@@ -2415,7 +2421,7 @@ function parse($TEXT, options) {
is_default = true;
next();
} else {
exported_names = import_names(false);
exported_names = map_names(false);
if (exported_names) {
if (is("name", "from")) {

View File

@@ -243,9 +243,22 @@ TreeTransformer.prototype = new TreeWalker;
self.expression = self.expression.transform(tw);
});
_(AST_Export, function(self, tw){
_(AST_NameMapping, function(self, tw) {
self.foreign_name = self.foreign_name.transform(tw);
self.name = self.name.transform(tw);
});
_(AST_Import, function(self, tw) {
if (self.imported_name) self.imported_name = self.imported_name.transform(tw);
if (self.imported_names) do_list(self.imported_names, tw);
self.module_name = self.module_name.transform(tw);
});
_(AST_Export, function(self, tw) {
if (self.exported_definition) self.exported_definition = self.exported_definition.transform(tw);
if (self.exported_value) self.exported_value = self.exported_value.transform(tw);
if (self.exported_names) do_list(self.exported_names, tw);
if (self.module_name) self.module_name = self.module_name.transform(tw);
});
_(AST_TemplateString, function(self, tw) {

97
test/compress/export.js Normal file
View File

@@ -0,0 +1,97 @@
issue_2038_1: {
options = {
toplevel: true,
unused: true,
}
mangle = {
toplevel: true,
}
input: {
export var V = 1;
export let L = 2;
export const C = 3;
}
expect: {
export var V = 1;
export let L = 2;
export const C = 3;
}
}
issue_2038_2: {
options = {
toplevel: true,
unused: true,
}
mangle = {
toplevel: true,
}
input: {
let LET = 1;
const CONST = 2;
var VAR = 3;
export { LET, CONST, VAR };
}
expect: {
let a = 1;
const c = 2;
var n = 3;
export { a as LET, c as CONST, n as VAR };
}
}
issue_2124: {
options = {
unused: true,
}
input: {
{
export var V = 1;
}
{
export let L = 2;
}
{
export const C = 3;
}
}
expect: {
{
export var V = 1;
}
{
export let L = 2;
}
{
export const C = 3;
}
}
}
issue_2126: {
mangle = {
toplevel: true,
}
input: {
import { foo as bar, cat as dog } from "stuff";
console.log(bar, dog);
export { bar as qux };
export { dog };
}
expect: {
import { foo as o, cat as f } from "stuff";
console.log(o, f);
export { o as qux };
export { f as dog };
}
}
beautify: {
beautify = {
beautify: true,
}
input: {
export { A as B, C as D };
}
expect_exact: "export { A as B, C as D };"
}

View File

@@ -2,8 +2,7 @@ var assert = require("assert");
var uglify = require("../node");
describe("Export", function() {
it ("Should parse export directives", function() {
it("Should parse export directives", function() {
var inputs = [
['export * from "a.js"', ['*'], "a.js"],
['export {A} from "a.js"', ['A'], "a.js"],
@@ -12,17 +11,17 @@ describe("Export", function() {
['export {A, B} from "a.js"', ['A', 'B'], "a.js"],
];
var test = function(code) {
function test(code) {
return uglify.parse(code);
};
}
var extractNames = function(symbols) {
function extractNames(symbols) {
var ret = [];
for (var i = 0; i < symbols.length; i++) {
ret.push(symbols[i].name.name)
ret.push(symbols[i].foreign_name.name);
}
return ret;
};
}
for (var i = 0; i < inputs.length; i++) {
var ast = test(inputs[i][0]);
@@ -34,7 +33,7 @@ describe("Export", function() {
assert(st instanceof uglify.AST_Export);
var actualNames = extractNames(st.exported_names);
assert.deepEqual(actualNames, names);
assert.equal(st.module_name.value, filename)
assert.equal(st.module_name.value, filename);
}
})
});
});
});