From 5f23328159bf7bb7eb4f3d9ed853a5156134a0be Mon Sep 17 00:00:00 2001 From: theMackabu Date: Thu, 19 Feb 2026 13:23:34 -0800 Subject: [PATCH] support string literal export/import names --- src/ant.c | 56 ++++++++++++++------------- tests/string-export-module.js | 9 +++++ tests/string-local-reexport-module.js | 1 + tests/string-reexport-module.js | 1 + tests/test_string_exports.js | 45 +++++++++++++++++++++ 5 files changed, 86 insertions(+), 26 deletions(-) create mode 100644 tests/string-export-module.js create mode 100644 tests/string-local-reexport-module.js create mode 100644 tests/string-reexport-module.js create mode 100644 tests/test_string_exports.js diff --git a/src/ant.c b/src/ant.c index 1d8dae0..bb8e17f 100644 --- a/src/ant.c +++ b/src/ant.c @@ -22913,28 +22913,42 @@ static jsval_t esm_make_file_url(struct js *js, const char *path) { return val; } +static bool esm_parse_name(struct js *js, const char **name, size_t *len, bool *is_string) { + if (next(js) == TOK_STRING) { + jsval_t sv = js_str_literal(js); + jsoff_t slen; jsoff_t soff = vstr(js, sv, &slen); + *name = (const char *)&js->mem[soff]; *len = slen; + if (is_string) *is_string = true; + } else if (next(js) == TOK_IDENTIFIER || next(js) == TOK_DEFAULT) { + *name = &js->code[js->toff]; + *len = js->tlen; + if (is_string) *is_string = false; + } else return false; + + js->consumed = 1; + return true; +} + static int esm_parse_named_imports(struct js *js, esm_import_binding_t *bindings, int max_bindings) { int count = 0; while (next(js) != TOK_RBRACE && count < max_bindings) { - if (next(js) != TOK_IDENTIFIER && next(js) != TOK_DEFAULT) { + bool import_is_string = false; + const char *import_name; + size_t import_len; + + if (!esm_parse_name(js, &import_name, &import_len, &import_is_string)) return -1; - } - const char *import_name = &js->code[js->toff]; - size_t import_len = js->tlen; - js->consumed = 1; const char *local_name = import_name; size_t local_len = import_len; if (next(js) == TOK_AS) { js->consumed = 1; - if (next(js) != TOK_IDENTIFIER && next(js) != TOK_DEFAULT) { + if (!esm_parse_name(js, &local_name, &local_len, NULL)) return -1; - } - local_name = &js->code[js->toff]; - local_len = js->tlen; - js->consumed = 1; + } else if (import_is_string) { + return -1; } bindings[count].import_name = import_name; @@ -23317,10 +23331,8 @@ static jsval_t js_export_stmt(struct js *js) { if (next(js) == TOK_AS) { js->consumed = 1; - EXPECT(TOK_IDENTIFIER); - alias_name = &js->code[js->toff]; - alias_len = js->tlen; - js->consumed = 1; + if (!esm_parse_name(js, &alias_name, &alias_len, NULL)) + return js_mkerr_typed(js, JS_ERR_SYNTAX, "expected identifier or string after 'as'"); } EXPECT(TOK_FROM); @@ -23364,23 +23376,15 @@ static jsval_t js_export_stmt(struct js *js) { while (next(js) != TOK_RBRACE) { if (spec_count >= 64) return js_mkerr(js, "too many export specifiers"); - if (next(js) != TOK_IDENTIFIER && next(js) != TOK_DEFAULT) { - return js_mkerr_typed(js, JS_ERR_SYNTAX, "expected identifier or 'default' in export list"); - } - specs[spec_count].local = &js->code[js->toff]; - specs[spec_count].local_len = js->tlen; + if (!esm_parse_name(js, &specs[spec_count].local, &specs[spec_count].local_len, NULL)) + return js_mkerr_typed(js, JS_ERR_SYNTAX, "expected identifier, string, or 'default' in export list"); specs[spec_count].exported = specs[spec_count].local; specs[spec_count].export_len = specs[spec_count].local_len; - js->consumed = 1; if (next(js) == TOK_AS) { js->consumed = 1; - if (next(js) != TOK_IDENTIFIER && next(js) != TOK_DEFAULT) { - return js_mkerr_typed(js, JS_ERR_SYNTAX, "expected identifier or 'default' after 'as'"); - } - specs[spec_count].exported = &js->code[js->toff]; - specs[spec_count].export_len = js->tlen; - js->consumed = 1; + if (!esm_parse_name(js, &specs[spec_count].exported, &specs[spec_count].export_len, NULL)) + return js_mkerr_typed(js, JS_ERR_SYNTAX, "expected identifier, string, or 'default' after 'as'"); } spec_count++; diff --git a/tests/string-export-module.js b/tests/string-export-module.js new file mode 100644 index 0000000..f119271 --- /dev/null +++ b/tests/string-export-module.js @@ -0,0 +1,9 @@ +const jq = "jquery"; +const foo = 42; +const bar = "hello"; +const baz = true; + +export { jq as "matrix" }; +export { foo as "foo-bar" }; +export { bar as "unicode \u0041" }; +export { baz as "default" }; diff --git a/tests/string-local-reexport-module.js b/tests/string-local-reexport-module.js new file mode 100644 index 0000000..ca02143 --- /dev/null +++ b/tests/string-local-reexport-module.js @@ -0,0 +1 @@ +export { "matrix" as "renamed" } from './string-export-module.js'; diff --git a/tests/string-reexport-module.js b/tests/string-reexport-module.js new file mode 100644 index 0000000..7bd6ee4 --- /dev/null +++ b/tests/string-reexport-module.js @@ -0,0 +1 @@ +export * as "nested" from './string-export-module.js'; diff --git a/tests/test_string_exports.js b/tests/test_string_exports.js new file mode 100644 index 0000000..3be0e85 --- /dev/null +++ b/tests/test_string_exports.js @@ -0,0 +1,45 @@ +// Test: String literal export/import names (ES2022) +// Tests export { x as "string" }, import { "string" as x }, export * as "string" + +console.log("=== Testing string literal export/import names ===\n"); + +let passed = 0; +let failed = 0; + +function assert(condition, msg) { + if (condition) { + console.log("✓", msg); + passed++; + } else { + console.log("✗", msg); + failed++; + } +} + +// Test 1: import { "string" as local } from module +async function main() { + // Basic string-named exports + const mod = await import('./string-export-module.js'); + + assert(mod["matrix"] === "jquery", 'export { jq as "matrix" }'); + assert(mod["foo-bar"] === 42, 'export { foo as "foo-bar" }'); + assert(mod["unicode A"] === "hello", 'export { bar as "unicode \\u0041" }'); + assert(mod["default"] === true, 'export { baz as "default" }'); + + // Test 2: export * as "string" from re-export + const remod = await import('./string-reexport-module.js'); + + assert(typeof remod["nested"] === "object", 'export * as "nested" produces object'); + assert(remod["nested"]["matrix"] === "jquery", 're-exported "matrix" accessible via "nested"'); + assert(remod["nested"]["foo-bar"] === 42, 're-exported "foo-bar" accessible via "nested"'); + + // Test 3: string as local name in export (valid only in re-exports) + const remod2 = await import('./string-local-reexport-module.js'); + + assert(remod2["renamed"] === "jquery", 'export { "matrix" as "renamed" } from re-export'); + + console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`); + if (failed > 0) process.exit(1); +} + +void main(); -- 2.51.2