Skip to content

export let reassignment is incorrectly tree-shaken (live binding broken in production build) #8916

@y-masuda-saraits

Description

@y-masuda-saraits

Description

When an export let variable is initialized as a no-op function and later reassigned inside another exported function, Rolldown's tree-shaking incorrectly removes both the reassignment and the call site — even though the reassignment contains real side effects (document.title assignment).

This violates ES module semantics where export let creates a live binding that should reflect reassignment to importers.

Reproduction

https://github.com/y-masuda-saraits/rolldown-treeshake-bug-repro

git clone https://github.com/y-masuda-saraits/rolldown-treeshake-bug-repro.git
cd rolldown-treeshake-bug-repro
npm install
npx vite build

Then inspect dist/assets/index-*.js. The relevant section in the output:

var setup = () => {};
var doWork = () => {};
setup();
doWork();

Source files (3 modules)

src/logger.js — exports a mutable binding and a function that reassigns it:

export let send = () => {}

export const setup = () => {
  send = (msg) => {
    document.title = msg   // side effect
    console.log('[send]', msg)
  }
}

src/consumer.js — imports and calls the live binding:

import { send } from './logger.js'

export const doWork = () => {
  send('hello from doWork')
}

src/main.js — entry point:

import { setup } from './logger.js'
import { doWork } from './consumer.js'

setup()
doWork()

Expected behavior

The bundled output should preserve:

  1. The send = (msg) => { document.title = msg; ... } reassignment inside setup
  2. The send('hello from doWork') call inside doWork

At runtime, document.title should be set to "hello from doWork".

Actual behavior

Rolldown replaces both setup and doWork with () => {}, completely removing:

  • The send reassignment (including the document.title side effect)
  • The send(...) call in doWork

The bundler appears to inline the initial value () => {} for the export let binding and then determine all call sites have no side effects, triggering cascading dead-code elimination.

Environment

  • Vite 8.0.3 (Rolldown bundled)
  • build.minify: false in vite.config.js (to keep output readable)
  • Windows 11, Node v22

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions