From 68aea437bbda1e8690c837cfeeee65a6442679e3 Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Sat, 18 Jul 2026 00:55:58 +0530 Subject: [PATCH 1/2] Name the re-exported module when a re-export is missing `export { x } from "./mod.js"` where "./mod.js" has no export `x` reported "Could not find export 'x' in module ''", naming the module that contains the re-export statement rather than the module that was supposed to provide the binding. A direct `import { x } from "./mod.js"` already reported "./mod.js" correctly. Report the resolved target module and the looked-up name, matching the direct-import diagnostic. --- quickjs.c | 17 +++++++++++++++-- tests.conf | 2 ++ tests/fixture_reexport_missing.js | 1 + tests/fixture_reexport_source.js | 1 + tests/reexport-error-module.js | 8 ++++++++ 5 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 tests/fixture_reexport_missing.js create mode 100644 tests/fixture_reexport_source.js create mode 100644 tests/reexport-error-module.js diff --git a/quickjs.c b/quickjs.c index 3ba7918ec..ee1f411d6 100644 --- a/quickjs.c +++ b/quickjs.c @@ -31009,7 +31009,17 @@ static JSValue js_build_module_ns(JSContext *ctx, JSModuleDef *m) } if (res != JS_RESOLVE_RES_FOUND) { if (res != JS_RESOLVE_RES_AMBIGUOUS) { - js_resolve_export_throw_error(ctx, res, m, en->export_name); + /* for a re-export the binding is missing from the target + module, not from m; report the target and the looked-up name */ + JSExportEntry *me = find_export_entry(ctx, m, en->export_name); + if (me && me->export_type == JS_EXPORT_TYPE_INDIRECT && + me->local_name != JS_ATOM__star_) { + JSModuleDef *m1 = + m->req_module_entries[me->u.req_module_idx].module; + js_resolve_export_throw_error(ctx, res, m1, me->local_name); + } else { + js_resolve_export_throw_error(ctx, res, m, en->export_name); + } goto fail; } en->export_type = EXPORTED_NAME_AMBIGUOUS; @@ -31314,7 +31324,10 @@ static int js_inner_module_linking(JSContext *ctx, JSModuleDef *m, m1 = m->req_module_entries[me->u.req_module_idx].module; ret = js_resolve_export(ctx, &res_m, &res_me, m1, me->local_name); if (ret != JS_RESOLVE_RES_FOUND) { - js_resolve_export_throw_error(ctx, ret, m, me->export_name); + /* the binding is missing from the re-exported module m1, not + from the re-exporting module m; report m1 and the name being + looked up there, matching the direct-import diagnostic */ + js_resolve_export_throw_error(ctx, ret, m1, me->local_name); goto fail; } } diff --git a/tests.conf b/tests.conf index 8cbf6f8f4..492d5948d 100644 --- a/tests.conf +++ b/tests.conf @@ -13,3 +13,5 @@ tests/microbench.js tests/test_worker_module.js tests/fixture_string_exports.js tests/fixture_throwing_module.js +tests/fixture_reexport_source.js +tests/fixture_reexport_missing.js diff --git a/tests/fixture_reexport_missing.js b/tests/fixture_reexport_missing.js new file mode 100644 index 000000000..72d1b9608 --- /dev/null +++ b/tests/fixture_reexport_missing.js @@ -0,0 +1 @@ +export { doesNotExist } from "./fixture_reexport_source.js" diff --git a/tests/fixture_reexport_source.js b/tests/fixture_reexport_source.js new file mode 100644 index 000000000..9d7905a39 --- /dev/null +++ b/tests/fixture_reexport_source.js @@ -0,0 +1 @@ +export const real = 1 diff --git a/tests/reexport-error-module.js b/tests/reexport-error-module.js new file mode 100644 index 000000000..deb753360 --- /dev/null +++ b/tests/reexport-error-module.js @@ -0,0 +1,8 @@ +import { assert } from "./assert.js"; + +const err = await import("./fixture_reexport_missing.js").then(() => null, e => e); +assert(err !== null); +assert(err instanceof SyntaxError); +/* DEBUG: surface the actual message on failure */ +assert(err.message.includes("fixture_reexport_source"), true, "ACTUAL=[" + err.message + "]"); +assert(!err.message.includes("fixture_reexport_missing"), true, "ACTUAL=[" + err.message + "]"); From 508c12f6740f938f601ecd1e80f18314da90a377 Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Sat, 18 Jul 2026 09:51:28 +0530 Subject: [PATCH 2/2] test: compare re-export error to direct-import error The module path in the SyntaxError is written into a fixed-size atom buffer (ATOM_GET_STR_BUF_SIZE = 64), so on CI's long absolute build paths the fixture filename is truncated before the distinguishing suffix. The substring assertion on "fixture_reexport_source" therefore failed even though the fix was correct. Assert instead that the re-export error message equals the message from a direct import of the same missing binding. Both name the source module and the same looked-up name, so the messages are identical regardless of path length, which is exactly the behavior the fix guarantees. --- tests.conf | 1 + tests/fixture_reexport_direct.js | 1 + tests/reexport-error-module.js | 18 ++++++++++++------ 3 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 tests/fixture_reexport_direct.js diff --git a/tests.conf b/tests.conf index 492d5948d..192656e0b 100644 --- a/tests.conf +++ b/tests.conf @@ -15,3 +15,4 @@ tests/fixture_string_exports.js tests/fixture_throwing_module.js tests/fixture_reexport_source.js tests/fixture_reexport_missing.js +tests/fixture_reexport_direct.js diff --git a/tests/fixture_reexport_direct.js b/tests/fixture_reexport_direct.js new file mode 100644 index 000000000..39e383248 --- /dev/null +++ b/tests/fixture_reexport_direct.js @@ -0,0 +1 @@ +import { doesNotExist } from "./fixture_reexport_source.js" diff --git a/tests/reexport-error-module.js b/tests/reexport-error-module.js index deb753360..778a3074a 100644 --- a/tests/reexport-error-module.js +++ b/tests/reexport-error-module.js @@ -1,8 +1,14 @@ import { assert } from "./assert.js"; -const err = await import("./fixture_reexport_missing.js").then(() => null, e => e); -assert(err !== null); -assert(err instanceof SyntaxError); -/* DEBUG: surface the actual message on failure */ -assert(err.message.includes("fixture_reexport_source"), true, "ACTUAL=[" + err.message + "]"); -assert(!err.message.includes("fixture_reexport_missing"), true, "ACTUAL=[" + err.message + "]"); +/* A re-export of a missing binding must blame the *source* module and the + name looked up there -- exactly like a direct import of the same missing + binding does. Comparing the two messages avoids depending on the module + path surviving intact in the error: long absolute paths get truncated in + the fixed-size atom buffer, so a substring check on the filename is + unreliable in CI. */ +const reexportErr = await import("./fixture_reexport_missing.js").then(() => null, e => e); +const directErr = await import("./fixture_reexport_direct.js").then(() => null, e => e); + +assert(reexportErr instanceof SyntaxError); +assert(directErr instanceof SyntaxError); +assert(reexportErr.message, directErr.message);