diff --git a/src/js_parser/visit/visit_expr.rs b/src/js_parser/visit/visit_expr.rs index 0092fdbd21f4..695871596ac0 100644 --- a/src/js_parser/visit/visit_expr.rs +++ b/src/js_parser/visit/visit_expr.rs @@ -929,6 +929,12 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O }, ); + if e_.optional_chain == Some(js_ast::OptionalChain::Start) + && matches!(e_.target.data, Data::ESpecial(E::Special::HotEnabled)) + { + e_.optional_chain = None; + } + match e_.index.data { Data::EPrivateIdentifier(mut private) => { let name = p.load_name_from_ref(private.ref_); @@ -1424,6 +1430,15 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O }, ); + // `import.meta.hot` is always defined when HMR is enabled, so drop + // the `?.` in `import.meta.hot?.X` and let `maybe_rewrite_property_access` + // lower it instead of leaving the throwing `indirectHot` proxy in place. + if e_.optional_chain == Some(js_ast::OptionalChain::Start) + && matches!(e_.target.data, Data::ESpecial(E::Special::HotEnabled)) + { + e_.optional_chain = None; + } + // 'require.resolve' -> .e_require_resolve_call_target if matches!(e_.target.data, Data::ERequireCallTarget) && e_.name == b"resolve" { // we do not need to call p.recordUsageOfRuntimeRequire(); because `require` diff --git a/test/bake/dev/hot.test.ts b/test/bake/dev/hot.test.ts index 92e158155dea..4858715313f7 100644 --- a/test/bake/dev/hot.test.ts +++ b/test/bake/dev/hot.test.ts @@ -388,6 +388,29 @@ devTest("import.meta.hot.dispose cleanup", { await c.expectMessage("Cleaning up", "Third setup"); }, }); +devTest("import.meta.hot with optional chaining", { + files: { + "index.html": emptyHtmlFile({ + scripts: ["index.ts"], + }), + "index.ts": ` + const data = import.meta.hot?.data; + data.count ??= 0; + import.meta.hot?.dispose(() => console.log("Cleaning up")); + import.meta.hot?.["on"]("bun:invalidate", () => {}); + import.meta.hot?.accept(); + console.log("Initial count: " + data.count++); + `, + }, + async test(dev) { + await using c = await dev.client("/"); + await c.expectMessage("Initial count: 0"); + await dev.writeNoChanges("index.ts"); + await c.expectMessage("Cleaning up", "Initial count: 1"); + await dev.writeNoChanges("index.ts"); + await c.expectMessage("Cleaning up", "Initial count: 2"); + }, +}); devTest("import.meta.hot invalid usage", { files: { "index.html": emptyHtmlFile({