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:
- The
send = (msg) => { document.title = msg; ... } reassignment inside setup
- 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
Description
When an
export letvariable 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.titleassignment).This violates ES module semantics where
export letcreates 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 buildThen inspect
dist/assets/index-*.js. The relevant section in the output:Source files (3 modules)
src/logger.js — exports a mutable binding and a function that reassigns it:
src/consumer.js — imports and calls the live binding:
src/main.js — entry point:
Expected behavior
The bundled output should preserve:
send = (msg) => { document.title = msg; ... }reassignment insidesetupsend('hello from doWork')call insidedoWorkAt runtime,
document.titleshould be set to"hello from doWork".Actual behavior
Rolldown replaces both
setupanddoWorkwith() => {}, completely removing:sendreassignment (including thedocument.titleside effect)send(...)call indoWorkThe bundler appears to inline the initial value
() => {}for theexport letbinding and then determine all call sites have no side effects, triggering cascading dead-code elimination.Environment
build.minify: falsein vite.config.js (to keep output readable)