Skip to content

Commit 8c526f6

Browse files
authored
Add ES2020 string export/import name support (#1188)
Support for ES2020 arbitrary module namespace identifier names, which allows using string literals as export/import names. Examples: ```js export { foo as "string-name" } import { "string-name" as foo } export * as "string-name" from "./mod.js" ```
1 parent d01ca44 commit 8c526f6

File tree

5 files changed

+100
-16
lines changed

5 files changed

+100
-16
lines changed

quickjs.c

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29308,6 +29308,23 @@ static __exception JSAtom js_parse_from_clause(JSParseState *s)
2930829308
return module_name;
2930929309
}
2931029310

29311+
static bool has_unmatched_surrogate(const uint16_t *s, size_t n)
29312+
{
29313+
size_t i;
29314+
29315+
for (i = 0; i < n; i++) {
29316+
if (is_lo_surrogate(s[i]))
29317+
return true;
29318+
if (!is_hi_surrogate(s[i]))
29319+
continue;
29320+
if (++i == n)
29321+
return true;
29322+
if (!is_lo_surrogate(s[i]))
29323+
return true;
29324+
}
29325+
return false;
29326+
}
29327+
2931129328
static __exception int js_parse_export(JSParseState *s)
2931229329
{
2931329330
JSContext *ctx = s->ctx;
@@ -29340,23 +29357,40 @@ static __exception int js_parse_export(JSParseState *s)
2934029357
switch(tok) {
2934129358
case '{':
2934229359
first_export = m->export_entries_count;
29360+
bool has_string_binding = false;
2934329361
while (s->token.val != '}') {
29344-
if (!token_is_ident(s->token.val)) {
29345-
js_parse_error(s, "identifier expected");
29346-
return -1;
29362+
if (token_is_ident(s->token.val)) {
29363+
local_name = JS_DupAtom(ctx, s->token.u.ident.atom);
29364+
} else if (s->token.val == TOK_STRING) {
29365+
local_name = JS_ValueToAtom(ctx, s->token.u.str.str);
29366+
if (local_name == JS_ATOM_NULL)
29367+
return -1;
29368+
has_string_binding = true;
29369+
} else {
29370+
return js_parse_error(s, "identifier or string expected");
2934729371
}
29348-
local_name = JS_DupAtom(ctx, s->token.u.ident.atom);
2934929372
export_name = JS_ATOM_NULL;
2935029373
if (next_token(s))
2935129374
goto fail;
2935229375
if (token_is_pseudo_keyword(s, JS_ATOM_as)) {
2935329376
if (next_token(s))
2935429377
goto fail;
29355-
if (!token_is_ident(s->token.val)) {
29356-
js_parse_error(s, "identifier expected");
29378+
if (token_is_ident(s->token.val)) {
29379+
export_name = JS_DupAtom(ctx, s->token.u.ident.atom);
29380+
} else if (s->token.val == TOK_STRING) {
29381+
JSString *p = JS_VALUE_GET_STRING(s->token.u.str.str);
29382+
if (p->is_wide_char && has_unmatched_surrogate(str16(p), p->len)) {
29383+
js_parse_error(s, "illegal export name");
29384+
return -1;
29385+
}
29386+
export_name = JS_ValueToAtom(ctx, s->token.u.str.str);
29387+
if (export_name == JS_ATOM_NULL) {
29388+
return -1;
29389+
}
29390+
} else {
29391+
js_parse_error(s, "identifier or string expected");
2935729392
goto fail;
2935829393
}
29359-
export_name = JS_DupAtom(ctx, s->token.u.ident.atom);
2936029394
if (next_token(s)) {
2936129395
fail:
2936229396
JS_FreeAtom(ctx, local_name);
@@ -29393,18 +29427,26 @@ static __exception int js_parse_export(JSParseState *s)
2939329427
me->export_type = JS_EXPORT_TYPE_INDIRECT;
2939429428
me->u.req_module_idx = idx;
2939529429
}
29430+
} else if (has_string_binding) {
29431+
// Without 'from' clause, string literals cannot be used as local binding names
29432+
return js_parse_error(s, "string export name only allowed with 'from' clause");
2939629433
}
2939729434
break;
2939829435
case '*':
2939929436
if (token_is_pseudo_keyword(s, JS_ATOM_as)) {
2940029437
/* export ns from */
2940129438
if (next_token(s))
2940229439
return -1;
29403-
if (!token_is_ident(s->token.val)) {
29404-
js_parse_error(s, "identifier expected");
29405-
return -1;
29440+
if (token_is_ident(s->token.val)) {
29441+
export_name = JS_DupAtom(ctx, s->token.u.ident.atom);
29442+
} else if (s->token.val == TOK_STRING) {
29443+
export_name = JS_ValueToAtom(ctx, s->token.u.str.str);
29444+
if (export_name == JS_ATOM_NULL) {
29445+
return -1;
29446+
}
29447+
} else {
29448+
return js_parse_error(s, "identifier or string expected");
2940629449
}
29407-
export_name = JS_DupAtom(ctx, s->token.u.ident.atom);
2940829450
if (next_token(s))
2940929451
goto fail1;
2941029452
module_name = js_parse_from_clause(s);
@@ -29578,11 +29620,15 @@ static __exception int js_parse_import(JSParseState *s)
2957829620
return -1;
2957929621

2958029622
while (s->token.val != '}') {
29581-
if (!token_is_ident(s->token.val)) {
29582-
js_parse_error(s, "identifier expected");
29583-
return -1;
29623+
if (token_is_ident(s->token.val)) {
29624+
import_name = JS_DupAtom(ctx, s->token.u.ident.atom);
29625+
} else if (s->token.val == TOK_STRING) {
29626+
import_name = JS_ValueToAtom(ctx, s->token.u.str.str);
29627+
if (import_name == JS_ATOM_NULL)
29628+
return -1;
29629+
} else {
29630+
return js_parse_error(s, "identifier or string expected expected");
2958429631
}
29585-
import_name = JS_DupAtom(ctx, s->token.u.ident.atom);
2958629632
local_name = JS_ATOM_NULL;
2958729633
if (next_token(s))
2958829634
goto fail;

test262.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ __proto__
4848
__setter__
4949
AggregateError
5050
align-detached-buffer-semantics-with-web-reality
51-
arbitrary-module-namespace-names=skip
51+
arbitrary-module-namespace-names
5252
array-find-from-last
5353
array-grouping
5454
Array.fromAsync

tests.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ tests/empty.js
88
tests/fixture_cyclic_import.js
99
tests/microbench.js
1010
tests/test_worker_module.js
11+
tests/fixture_string_exports.js

tests/fixture_string_exports.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// ES2020 string export names test fixture
2+
export const regularExport = "regular";
3+
const value1 = "value-1";
4+
const value2 = "value-2";
5+
6+
// String export names (ES2020)
7+
export { value1 as "string-export-1" };
8+
export { value2 as "string-export-2" };
9+
10+
// Mixed: regular and string exports
11+
const mixed = "mixed-value";
12+
export { mixed as normalName, mixed as "string-name" };

tests/test_string_exports.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Test ES2020 string export/import names
2+
import { assert } from "./assert.js";
3+
import * as mod from "./fixture_string_exports.js";
4+
5+
// Test string import names
6+
import { "string-export-1" as str1 } from "./fixture_string_exports.js";
7+
import { "string-export-2" as str2 } from "./fixture_string_exports.js";
8+
import { "string-name" as strMixed } from "./fixture_string_exports.js";
9+
10+
// Test regular imports still work
11+
import { regularExport, normalName } from "./fixture_string_exports.js";
12+
13+
// Verify values
14+
assert(str1, "value-1");
15+
assert(str2, "value-2");
16+
assert(strMixed, "mixed-value");
17+
assert(regularExport, "regular");
18+
assert(normalName, "mixed-value");
19+
20+
// Verify module namespace has string-named exports
21+
assert(mod["string-export-1"], "value-1");
22+
assert(mod["string-export-2"], "value-2");
23+
assert(mod["string-name"], "mixed-value");
24+
assert(mod.regularExport, "regular");
25+
assert(mod.normalName, "mixed-value");

0 commit comments

Comments
 (0)