Skip to content

var declarations inside if blocks not hoisted when using __esm wrappers #4348

Closed

Description

@gordonwoodhull

Full disclosure: this issue was identified and minimum repro was isolated by Claude Code.

It's a real issue we ran into building Quarto, triggered by upstream dependencies jszip and @jspm/core.

When esbuild creates __esm lazy initialization wrappers (triggered by CommonJS requiring ESM), it fails to hoist var declarations from inline if statement blocks to module scope.

Source pattern

if (condition) var b = getValue();

Current output (buggy)

function useB() {
  return b();  // ReferenceError: b is not defined
}

var init_module = __esm({
  "module.js"() {
    if (condition) var b = getValue();  // b scoped to init function
  }
});

Expected output

var b;  // Hoisted to module scope

function useB() {
  return b();  // OK
}

var init_module = __esm({
  "module.js"() {
    if (condition) b = getValue();  // Assignment only
  }
});

Reproduction

https://github.com/gordonwoodhull/esbuild-not-hoisting-conditional-vars

It is not possible to reproduce this bug in the playground since the bug requires:

  • Multi-file setup (CommonJS requiring ESM)
  • Actual bundling that triggers __esm wrappers
  • The specific module format interaction

The repository contains two reproductions:

1. flat/ - Plain esbuild (simpler)

Uses pre-downloaded JSPM CDN files with plain esbuild.

cd flat/
npm install
npx esbuild entry-cjs-real-jspm.js --bundle --outfile=bundle-cjs-real-jspm.js
npx eslint bundle-cjs-real-jspm.js  # Shows 16 errors
node bundle-cjs-real-jspm.js        # ReferenceError: b is not defined

2. bundled/ - With esbuild-deno-loader (matches real-world usage)

Uses @luca/esbuild-deno-loader plugin with mixed CommonJS/ESM setup.

cd bundled/
npm install
deno run -A bundle-with-deno-loader.ts
npx eslint bundle-hybrid.js  # Shows 18 errors

Impact

This affects:

  • Projects with mixed CommonJS/ESM modules
  • Libraries using minified inline var declarations (e.g., @jspm/core via JSPM CDN)
  • Any code where esbuild generates __esm wrappers

The bug is often masked by defensive programming. In our environment it does not cause an actual problem at runtime, but it causes eslint to fail, requiring globals to be declared.

Related

This appears related to the fix in esbuild 0.12.17 (PR #1455) for var hoisting in for loop initializers, but if blocks weren't covered.

Environment

  • esbuild version: 0.25.10 (bug present)
  • Also tested: 0.20.2 (bug present)

Activity

  1. evanw commented on Dec 1, 2025

    @evanw
    Owner

    Thanks for the report. I can reproduce the problem. Here's a live repro: link

  2. evanw commented on Dec 3, 2025

    @evanw
    Owner

    Note for myself: This is happening because esbuild doesn't introduce a scope in this case, since there isn't a block. The lack of a scope is not otherwise a problem since let and const aren't allowed in this position. But the code to hoist var exits early if it thinks it's a top-level statement, which it checks by checking whether the current scope is the module scope.

  3. added a commit that references this issue on Dec 3, 2025
    7a72735
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions