From f814e9dd0711edef8ef588e98a1cb61a4ceda467 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 21 Jul 2026 22:07:00 +0000 Subject: [PATCH 1/4] Fix import.meta.hot?.X throwing in dev server The guarded form `import.meta.hot?.accept()` was being lowered to `hmr.indirectHot?.accept()` in the dev server because the optional chain on the property access prevented `maybe_rewrite_property_access` from running. `indirectHot` is a truthy Proxy whose every property get throws, so the optional chain could not short-circuit and the module threw on evaluation. Since `import.meta.hot` is always defined when HMR is enabled, the `?.` guard is a no-op. Clear it after the target resolves to `HotEnabled` so the existing direct-access rewrite fires and the expression lowers to the appropriate HMR module member. --- src/js_parser/visit/visit_expr.rs | 18 ++++++++++++++++++ test/bake/dev/hot.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/js_parser/visit/visit_expr.rs b/src/js_parser/visit/visit_expr.rs index 0092fdbd21f4..539dc766f236 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,18 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O }, ); + // `import.meta.hot?.accept()` should be treated like a direct + // `import.meta.hot.accept()` in development. `import.meta.hot` is + // always defined when HMR is enabled, so the `?.` guard is a no-op + // and would otherwise prevent `maybe_rewrite_property_access` from + // lowering the access to the HMR module member, 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..c686ff8839de 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({ From 37eddec3fe1c1efb4d7e32352323e85c906fc3aa Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 21 Jul 2026 22:11:19 +0000 Subject: [PATCH 2/4] Shorten comment --- src/js_parser/visit/visit_expr.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/js_parser/visit/visit_expr.rs b/src/js_parser/visit/visit_expr.rs index 539dc766f236..695871596ac0 100644 --- a/src/js_parser/visit/visit_expr.rs +++ b/src/js_parser/visit/visit_expr.rs @@ -1430,12 +1430,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O }, ); - // `import.meta.hot?.accept()` should be treated like a direct - // `import.meta.hot.accept()` in development. `import.meta.hot` is - // always defined when HMR is enabled, so the `?.` guard is a no-op - // and would otherwise prevent `maybe_rewrite_property_access` from - // lowering the access to the HMR module member, leaving the - // throwing `indirectHot` proxy in place. + // `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)) { From 13ba6f9b06df189f04e8dd3b148f26b9cd8c711f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 21 Jul 2026 22:24:39 +0000 Subject: [PATCH 3/4] Add bracket-access coverage for e_index path --- test/bake/dev/hot.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/bake/dev/hot.test.ts b/test/bake/dev/hot.test.ts index c686ff8839de..4858715313f7 100644 --- a/test/bake/dev/hot.test.ts +++ b/test/bake/dev/hot.test.ts @@ -397,7 +397,7 @@ devTest("import.meta.hot with optional chaining", { 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?.["on"]("bun:invalidate", () => {}); import.meta.hot?.accept(); console.log("Initial count: " + data.count++); `, From b476f283875ce7a55a155ebfae3dc1bffe5bf888 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 21 Jul 2026 22:40:57 +0000 Subject: [PATCH 4/4] ci: retrigger