support string namespace in import & export (#5570)

This commit is contained in:
Alex Lam S.L
2022-07-19 22:55:38 +01:00
committed by GitHub
parent f0120e90b6
commit d67daa8314
9 changed files with 295 additions and 134 deletions

View File

@@ -1366,34 +1366,29 @@ var AST_ExportDefault = DEFNODE("ExportDefault", "body", {
},
}, AST_Statement);
var AST_ExportForeign = DEFNODE("ExportForeign", "aliases keys path quote", {
var AST_ExportForeign = DEFNODE("ExportForeign", "aliases keys path", {
$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",
aliases: "[AST_String*] array of aliases to export",
keys: "[AST_String*] array of keys to import",
path: "[AST_String] the path to import module",
},
_equals: function(node) {
return this.path == node.path
&& list_equals(this.aliases, node.aliases)
&& list_equals(this.keys, node.keys);
return this.path.equals(node.path)
&& all_equals(this.aliases, node.aliases)
&& all_equals(this.keys, node.keys);
},
_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");
if (!(name instanceof AST_String)) throw new Error("aliases must contain AST_String");
});
this.keys.forEach(function(name) {
if (typeof name != "string") throw new Error("keys must contain string");
if (!(name instanceof AST_String)) throw new Error("keys must contain AST_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);
}
if (!(this.path instanceof AST_String)) throw new Error("path must be AST_String");
},
}, AST_Statement);
@@ -1420,17 +1415,16 @@ var AST_ExportReferences = DEFNODE("ExportReferences", "properties", {
},
}, AST_Statement);
var AST_Import = DEFNODE("Import", "all default path properties quote", {
var AST_Import = DEFNODE("Import", "all default path properties", {
$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",
path: "[AST_String] the path to import module",
properties: "[(AST_SymbolImport*)?] array of aliases, or null if not specified",
quote: "[string?] the original quote character",
},
_equals: function(node) {
return this.path == node.path
return this.path.equals(node.path)
&& prop_equals(this.all, node.all)
&& prop_equals(this.default, node.default)
&& !this.properties == !node.properties
@@ -1453,16 +1447,12 @@ var AST_Import = DEFNODE("Import", "all default path properties quote", {
}
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 (this.default.key.value !== "") throw new Error("invalid default key: " + this.default.key.value);
}
if (typeof this.path != "string") throw new Error("path must be string");
if (!(this.path instanceof AST_String)) throw new Error("path must be AST_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");
if (!/^["']$/.test(this.quote)) throw new Error("invalid quote: " + this.quote);
}
},
}, AST_Statement);
@@ -2005,14 +1995,14 @@ var AST_SymbolConst = DEFNODE("SymbolConst", null, {
var AST_SymbolImport = DEFNODE("SymbolImport", "key", {
$documentation: "Symbol defined by an `import` statement",
$propdoc: {
key: "[string] the original `export` name",
key: "[AST_String] the original `export` name",
},
_equals: function(node) {
return this.name == node.name
&& this.key == node.key;
&& this.key.equals(node.key);
},
_validate: function() {
if (typeof this.key != "string") throw new Error("key must be string");
if (!(this.key instanceof AST_String)) throw new Error("key must be AST_String");
},
}, AST_SymbolConst);
@@ -2066,14 +2056,14 @@ var AST_SymbolRef = DEFNODE("SymbolRef", "fixed in_arg redef", {
var AST_SymbolExport = DEFNODE("SymbolExport", "alias", {
$documentation: "Reference in an `export` statement",
$propdoc: {
alias: "[string] the `export` alias",
alias: "[AST_String] the `export` alias",
},
_equals: function(node) {
return this.name == node.name
&& this.alias == node.alias;
&& this.alias.equals(node.alias);
},
_validate: function() {
if (typeof this.alias != "string") throw new Error("alias must be string");
if (!(this.alias instanceof AST_String)) throw new Error("alias must be AST_String");
},
}, AST_SymbolRef);

View File

@@ -294,7 +294,7 @@ Compressor.prototype.compress = function(node) {
function export_symbol(sym) {
if (!(sym instanceof AST_SymbolDeclaration)) return;
var node = make_node(AST_SymbolExport, sym, sym);
node.alias = node.name;
node.alias = make_node(AST_String, node, { value: node.name });
props.push(node);
}
});

View File

@@ -316,13 +316,22 @@
});
},
ExportAllDeclaration: function(M) {
var alias = M.exported ? read_name(M.exported) : "*";
var start = my_start_token(M);
var end = my_end_token(M);
return new AST_ExportForeign({
start: my_start_token(M),
end: my_end_token(M),
aliases: [ alias ],
keys: [ "*" ],
path: M.source.value,
start: start,
end: end,
aliases: [ M.exported ? from_moz_alias(M.exported) : new AST_String({
start: start,
value: "*",
end: end,
}) ],
keys: [ new AST_String({
start: start,
value: "*",
end: end,
}) ],
path: from_moz(M.source),
});
},
ExportDefaultDeclaration: function(M) {
@@ -359,15 +368,15 @@
if (M.source) {
var aliases = [], keys = [];
M.specifiers.forEach(function(prop) {
aliases.push(read_name(prop.exported));
keys.push(read_name(prop.local));
aliases.push(from_moz_alias(prop.exported));
keys.push(from_moz_alias(prop.local));
});
return new AST_ExportForeign({
start: my_start_token(M),
end: my_end_token(M),
aliases: aliases,
keys: keys,
path: M.source.value,
path: from_moz(M.source),
});
}
return new AST_ExportReferences({
@@ -375,38 +384,48 @@
end: my_end_token(M),
properties: M.specifiers.map(function(prop) {
var sym = new AST_SymbolExport(from_moz(prop.local));
sym.alias = read_name(prop.exported);
sym.alias = from_moz_alias(prop.exported);
return sym;
}),
});
},
ImportDeclaration: function(M) {
var start = my_start_token(M);
var end = my_end_token(M);
var all = null, def = null, props = null;
M.specifiers.forEach(function(prop) {
var sym = new AST_SymbolImport(from_moz(prop.local));
switch (prop.type) {
case "ImportDefaultSpecifier":
def = sym;
def.key = "";
def.key = new AST_String({
start: start,
value: "",
end: end,
});
break;
case "ImportNamespaceSpecifier":
all = sym;
all.key = "*";
all.key = new AST_String({
start: start,
value: "*",
end: end,
});
break;
default:
sym.key = prop.imported.name || syn.name;
sym.key = from_moz_alias(prop.imported);
if (!props) props = [];
props.push(sym);
break;
}
});
return new AST_Import({
start: my_start_token(M),
end: my_end_token(M),
start: start,
end: end,
all: all,
default: def,
properties: props,
path: M.source.value,
path: from_moz(M.source),
});
},
ImportExpression: function(M) {
@@ -797,38 +816,26 @@
});
def_to_moz(AST_ExportForeign, function To_Moz_ExportAllDeclaration_ExportNamedDeclaration(M) {
if (M.keys[0] == "*") return {
if (M.keys[0].value == "*") return {
type: "ExportAllDeclaration",
exported: M.aliases[0] == "*" ? null : {
type: "Identifier",
name: M.aliases[0],
},
source: {
type: "Literal",
value: M.path,
},
exported: M.aliases[0].value == "*" ? null : to_moz_alias(M.aliases[0]),
source: to_moz(M.path),
};
var specifiers = [];
for (var i = 0; i < M.aliases.length; i++) {
specifiers.push({
specifiers.push(set_moz_loc({
start: M.keys[i].start,
end: M.aliases[i].end,
}, {
type: "ExportSpecifier",
exported: {
type: "Identifier",
name: M.aliases[i],
},
local: {
type: "Identifier",
name: M.keys[i],
},
});
local: to_moz_alias(M.keys[i]),
exported: to_moz_alias(M.aliases[i]),
}));
}
return {
type: "ExportNamedDeclaration",
specifiers: specifiers,
source: {
type: "Literal",
value: M.path,
},
source: to_moz(M.path),
};
});
@@ -836,44 +843,41 @@
return {
type: "ExportNamedDeclaration",
specifiers: M.properties.map(function(prop) {
return {
return set_moz_loc({
start: prop.start,
end: prop.alias.end,
}, {
type: "ExportSpecifier",
local: to_moz(prop),
exported: {
type: "Identifier",
name: prop.alias,
},
};
exported: to_moz_alias(prop.alias),
});
}),
};
});
def_to_moz(AST_Import, function To_Moz_ImportDeclaration(M) {
var specifiers = M.properties ? M.properties.map(function(prop) {
return {
return set_moz_loc({
start: prop.key.start,
end: prop.end,
}, {
type: "ImportSpecifier",
local: to_moz(prop),
imported: {
type: "Identifier",
name: prop.key,
},
};
imported: to_moz_alias(prop.key),
});
}) : [];
if (M.all) specifiers.unshift({
if (M.all) specifiers.unshift(set_moz_loc(M.all, {
type: "ImportNamespaceSpecifier",
local: to_moz(M.all),
});
if (M.default) specifiers.unshift({
}));
if (M.default) specifiers.unshift(set_moz_loc(M.default, {
type: "ImportDefaultSpecifier",
local: to_moz(M.default),
});
}));
return {
type: "ImportDeclaration",
specifiers: specifiers,
source: {
type: "Literal",
value: M.path,
},
source: to_moz(M.path),
};
});
@@ -1220,6 +1224,14 @@
return node;
}
function from_moz_alias(moz) {
return new AST_String({
start: my_start_token(moz),
value: read_name(moz),
end: my_end_token(moz),
});
}
AST_Node.from_mozilla_ast = function(node) {
var save_stack = FROM_MOZ_STACK;
FROM_MOZ_STACK = [];
@@ -1271,6 +1283,13 @@
return node != null ? node.to_mozilla_ast() : null;
}
function to_moz_alias(alias) {
return is_identifier_string(alias.value) ? set_moz_loc(alias, {
type: "Identifier",
name: alias.value,
}) : to_moz(alias);
}
function to_moz_block(node) {
return {
type: "BlockStatement",

View File

@@ -1061,6 +1061,14 @@ function OutputStream(options) {
}
output.semicolon();
});
function print_alias(alias, output) {
var value = alias.value;
if (value == "*" || is_identifier_string(value)) {
output.print_name(value);
} else {
output.print_string(value, alias.quote);
}
}
DEFPRINT(AST_ExportForeign, function(output) {
var self = this;
output.print("export");
@@ -1068,7 +1076,7 @@ function OutputStream(options) {
var len = self.keys.length;
if (len == 0) {
print_braced_empty(self, output);
} else if (self.keys[0] == "*") {
} else if (self.keys[0].value == "*") {
print_entry(0);
} else output.with_block(function() {
output.indent();
@@ -1084,18 +1092,18 @@ function OutputStream(options) {
output.space();
output.print("from");
output.space();
output.print_string(self.path, self.quote);
self.path.print(output);
output.semicolon();
function print_entry(index) {
var alias = self.aliases[index];
var key = self.keys[index];
output.print_name(key);
if (alias != key) {
print_alias(key, output);
if (alias.value != key.value) {
output.space();
output.print("as");
output.space();
output.print_name(alias);
print_alias(alias, output);
}
}
});
@@ -1124,7 +1132,7 @@ function OutputStream(options) {
output.print("from");
output.space();
}
output.print_string(self.path, self.quote);
self.path.print(output);
output.semicolon();
});
@@ -1734,19 +1742,19 @@ function OutputStream(options) {
var name = get_symbol_name(self);
output.print_name(name);
var alias = self.alias;
if (alias != name) {
if (alias.value != name) {
output.space();
output.print("as");
output.space();
output.print_name(alias);
print_alias(alias, output);
}
});
DEFPRINT(AST_SymbolImport, function(output) {
var self = this;
var name = get_symbol_name(self);
var key = self.key;
if (key && key != name) {
output.print_name(key);
if (key.value && key.value != name) {
print_alias(key, output);
output.space();
output.print("as");
output.space();

View File

@@ -1442,28 +1442,41 @@ function parse($TEXT, options) {
}
function is_alias() {
return is("name") || is_identifier_string(S.token.value);
return is("name") || is("string") || is_identifier_string(S.token.value);
}
function make_string(token) {
return new AST_String({
start: token,
quote: token.quote,
value: token.value,
end: token,
});
}
function as_path() {
var path = S.token;
expect_token("string");
semicolon();
return make_string(path);
}
function export_() {
if (is("operator", "*")) {
var key = S.token;
var alias = key;
next();
var alias = "*";
if (is("name", "as")) {
next();
if (!is_alias()) expect_token("name");
alias = S.token.value;
alias = S.token;
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,
aliases: [ make_string(alias) ],
keys: [ make_string(key) ],
path: as_path(),
});
}
if (is("punc", "{")) {
@@ -1477,26 +1490,20 @@ function parse($TEXT, options) {
if (is("name", "as")) {
next();
if (!is_alias()) expect_token("name");
aliases.push(S.token.value);
aliases.push(S.token);
next();
} else {
aliases.push(key.value);
aliases.push(key);
}
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,
aliases: aliases.map(make_string),
keys: keys.map(make_string),
path: as_path(),
});
}
semicolon();
@@ -1504,7 +1511,7 @@ function parse($TEXT, options) {
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];
sym.alias = make_string(aliases[index]);
return sym;
}),
});
@@ -1594,26 +1601,42 @@ function parse($TEXT, options) {
var all = null;
var def = as_symbol(AST_SymbolImport, true);
var props = null;
if (def ? (def.key = "", is("punc", ",") && next()) : !is("string")) {
var cont;
if (def) {
def.key = new AST_String({
start: def.start,
value: "",
end: def.end,
});
if (cont = is("punc", ",")) next();
} else {
cont = !is("string");
}
if (cont) {
if (is("operator", "*")) {
var key = S.token;
next();
expect_token("name", "as");
all = as_symbol(AST_SymbolImport);
all.key = "*";
all.key = make_string(key);
} else {
expect("{");
props = [];
while (is_alias()) {
var alias;
if (is_token(peek(), "name", "as")) {
var key = S.token.value;
var key = S.token;
next();
next();
alias = as_symbol(AST_SymbolImport);
alias.key = key;
alias.key = make_string(key);
} else {
alias = as_symbol(AST_SymbolImport);
alias.key = alias.name;
alias.key = new AST_String({
start: alias.start,
value: alias.name,
end: alias.end,
});
}
props.push(alias);
if (!is("punc", "}")) expect(",");
@@ -1622,15 +1645,11 @@ function parse($TEXT, options) {
}
}
if (all || def || props) expect_token("name", "from");
var path = S.token;
expect_token("string");
semicolon();
return new AST_Import({
all: all,
default: def,
path: path.value,
path: as_path(),
properties: props,
quote: path.quote,
});
}

View File

@@ -109,6 +109,17 @@ foreign: {
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";'
}
non_identifiers: {
beautify = {
quote_style: 3,
}
input: {
export * as "42" from 'foo';
export { '42', "delete" as 'foo' } from "bar";
}
expect_exact: "export*as\"42\"from'foo';export{'42',delete as foo}from\"bar\";"
}
same_quotes: {
beautify = {
beautify: true,

View File

@@ -40,6 +40,17 @@ default_keys: {
expect_exact: 'import foo,{bar}from"baz";'
}
non_identifiers: {
beautify = {
quote_style: 3,
}
input: {
import { '42' as foo } from "bar";
import { "foo" as bar } from 'baz';
}
expect_exact: "import{'42'as foo}from\"bar\";import{foo as bar}from'baz';"
}
dynamic: {
input: {
(async a => await import(a))("foo").then(bar);

View File

@@ -1,3 +1,4 @@
var acorn = require("acorn");
var assert = require("assert");
var UglifyJS = require("../node");
@@ -25,6 +26,7 @@ describe("export", function() {
"export { * };",
"export { * as A };",
"export { 42 as A };",
"export { 'A' as B };",
"export { A as B-C };",
"export { default as A };",
].forEach(function(code) {
@@ -51,8 +53,11 @@ describe("export", function() {
it("Should reject invalid `export ... from ...` statement syntax", function() {
[
"export from 'path';",
"export A from 'path';",
"export * from `path`;",
"export 'A' from 'path';",
"export A as B from 'path';",
"export 'A' as B from 'path';",
"export default from 'path';",
"export { A }, B from 'path';",
"export * as A, B from 'path';",
@@ -128,4 +133,49 @@ describe("export", function() {
});
});
});
it("Should interoperate with ESTree correctly", function() {
[
"export var A = 42;",
"export default (class A {});",
"var A; export { A as '42' };",
"export { '42' } from 'path';",
"export * as '42' from 'path';",
].forEach(function(code) {
var ast = UglifyJS.parse(code);
try {
var spidermonkey = ast.to_mozilla_ast();
} catch (ex) {
assert.fail("[to_mozilla_ast] " + ex.stack);
}
try {
var ast2 = UglifyJS.AST_Node.from_mozilla_ast(spidermonkey);
} catch (ex) {
assert.fail("[from_mozilla_ast] " + ex.stack);
}
assert.strictEqual(ast2.print_to_string(), ast.print_to_string(), [
"spidermonkey:",
ast.print_to_string(),
ast2.print_to_string(),
].join("\n"));
try {
var acorn_est = acorn.parse(code, {
ecmaVersion: "latest",
locations: true,
sourceType: "module",
});
} catch (ex) {
assert.fail("[acorn.parse] " + ex.stack);
}
try {
var ast3 = UglifyJS.AST_Node.from_mozilla_ast(acorn_est);
} catch (ex) {
assert.fail("[from_acorn] " + ex.stack);
}
assert.strictEqual(ast3.print_to_string(), ast.print_to_string(), [
"acorn:",
ast.print_to_string(),
ast3.print_to_string(),
].join("\n"));
});
});
});

View File

@@ -1,3 +1,4 @@
var acorn = require("acorn");
var assert = require("assert");
var UglifyJS = require("../node");
@@ -12,14 +13,21 @@ describe("import", function() {
"import from 'path';",
"if (0) import 'path';",
"import * from 'path';",
"import 'A' from 'path';",
"import A-B from 'path';",
"import A as B from 'path';",
"import { A }, B from 'path';",
"import * as 'A' from 'path';",
"import * as A-B from 'path';",
"import * as A, B from 'path';",
"import * as A, {} from 'path';",
"import { * as A } from 'path';",
"import { * as 'A' } from 'path';",
"import { * as A-B } from 'path';",
"function f() { import 'path'; }",
"import { 42 as A } from 'path';",
"import { A-B as C } from 'path';",
"import { 'A' as 'B' } from 'path';",
].forEach(function(code) {
assert.throws(function() {
UglifyJS.parse(code);
@@ -53,4 +61,49 @@ describe("import", function() {
});
});
});
it("Should interoperate with ESTree correctly", function() {
[
"import A from 'path';",
"import * as A from 'path';",
"import A, * as B from 'path';",
"import { '42' as A, B } from 'path';",
"import A, { '42' as B } from 'path';",
].forEach(function(code) {
var ast = UglifyJS.parse(code);
try {
var spidermonkey = ast.to_mozilla_ast();
} catch (ex) {
assert.fail("[to_mozilla_ast] " + ex.stack);
}
try {
var ast2 = UglifyJS.AST_Node.from_mozilla_ast(spidermonkey);
} catch (ex) {
assert.fail("[from_mozilla_ast] " + ex.stack);
}
assert.strictEqual(ast2.print_to_string(), ast.print_to_string(), [
"spidermonkey:",
ast.print_to_string(),
ast2.print_to_string(),
].join("\n"));
try {
var acorn_est = acorn.parse(code, {
ecmaVersion: "latest",
locations: true,
sourceType: "module",
});
} catch (ex) {
assert.fail("[acorn.parse] " + ex.stack);
}
try {
var ast3 = UglifyJS.AST_Node.from_mozilla_ast(acorn_est);
} catch (ex) {
assert.fail("[from_acorn] " + ex.stack);
}
assert.strictEqual(ast3.print_to_string(), ast.print_to_string(), [
"acorn:",
ast.print_to_string(),
ast3.print_to_string(),
].join("\n"));
});
});
});