From 342151609b3d9683ef5e92bb30a894aa0505af80 Mon Sep 17 00:00:00 2001 From: Boshen Date: Fri, 30 Oct 2026 11:36:39 +0800 Subject: [PATCH] fix(parser): preserve default import names in reexports (#28442) Record `default` as the imported name when reexporting a default import, including aliases and exports declared before the import. Validated with scoped Rust checks, full conformance, parser bindings, allocation tracking. AI-assisted (Codex). --- crates/oxc_parser/src/module_record.rs | 37 +++++++++++++++++++++----- napi/parser/test/parse.test.ts | 2 +- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/crates/oxc_parser/src/module_record.rs b/crates/oxc_parser/src/module_record.rs index 98c94f14f7..03f27874f6 100644 --- a/crates/oxc_parser/src/module_record.rs +++ b/crates/oxc_parser/src/module_record.rs @@ -163,12 +163,9 @@ impl<'a> ModuleRecordBuilder<'a> { ImportImportName::Name(name) => { ExportImportName::Name(name.clone()) } - // `import d from "mod"` - // `export { d }` - // ^ this is local_name of ie - ImportImportName::Default(_) => { - ExportImportName::Name(ie.local_name.clone()) - } + ImportImportName::Default(span) => ExportImportName::Name( + NameSpan::new("default".into(), *span), + ), ImportImportName::NamespaceObject => unreachable!(), }, export_name: ee.export_name.clone(), @@ -714,6 +711,34 @@ mod module_record_tests { assert_eq!(module_record.indirect_export_entries[0], export_entry); } + #[test] + fn reexport_imported_default() { + for (source, exported) in [ + ("import d from 'mod'; export { d };", "d"), + ("export { d as renamed }; import d from 'mod';", "renamed"), + ("import d from 'mod'; export { d as default };", "default"), + ] { + let allocator = Allocator::default(); + let ret = Parser::new(&allocator, source, SourceType::mjs()).parse(); + assert!(ret.diagnostics.is_empty()); + assert!(ret.module_record.local_export_entries.is_empty()); + assert_eq!(ret.module_record.indirect_export_entries.len(), 1); + let entry = &ret.module_record.indirect_export_entries[0]; + let import_start = u32::try_from(source.find("import d").unwrap()).unwrap() + 7; + assert_eq!( + entry.import_name, + ExportImportName::Name(NameSpan::new( + "default".into(), + Span::sized(import_start, 1), + )) + ); + let ExportExportName::Name(name) = &entry.export_name else { panic!() }; + assert_eq!(name.name, exported); + assert_eq!(entry.module_request.as_ref().unwrap().name, "mod"); + assert!(matches!(entry.local_name, ExportLocalName::Null)); + } + } + #[test] fn indirect_export_entries() { let allocator = Allocator::default(); diff --git a/napi/parser/test/parse.test.ts b/napi/parser/test/parse.test.ts index 9930ae2517..640d5c598f 100644 --- a/napi/parser/test/parse.test.ts +++ b/napi/parser/test/parse.test.ts @@ -801,7 +801,7 @@ describe("UTF-16 span", () => { "importName": { "end": 13, "kind": "Name", - "name": "x", + "name": "default", "start": 12, }, "isType": false,