Reproduction link or steps
Minimal, self-contained (pure ESM, no CJS/barrel/dynamic-import). 4 files:
vite.config.js
export default { experimental: { bundledDev: true } };
index.html
<!doctype html><html><body><div id="app"></div>
<script type="module" src="/src/main.js"></script></body></html>
src/main.js
import { defaults } from './a.js';
document.getElementById('app').textContent = 'button=' + defaults.button.name;
src/a.js
import { B } from './b.js';
// top-level (module-init) use of the cyclic import — this is what breaks
export const defaults = { button: B };
src/b.js
import { defaults } from './a.js'; // cycle back to a.js
export class B {}
export const usesA = () => defaults; // used only inside a fn -> bound fine
Run vite (dev), open the page.
What is expected?
Page renders button=B. B resolves to the class exported by b.js, as it does
in classic Vite dev (rollup+esbuild) and in vite build.
What is actually happening?
ReferenceError: B is not defined.
The served entry chunk /assets/index.js emits:
var defaults = { button: B } // B referenced...
…but class B is not declared anywhere in that chunk (it lives in b.js's
separate lazy chunk). So the value-position reference to the cyclic import is
emitted unbound.
Key detail: only imports used during module initialization (top-level) break —
export const x = { button: B }, class X extends B, const y = makeThing(B), etc.
Sibling imports from the same module that are referenced only inside function
bodies are bound correctly. The unbound one happens to participate in an import
cycle.
This is experimental.bundledDev only; experimental.devMode.lazy: false does not
change it. Classic dev avoids it (esbuild optimizeDeps flattens dep cycles + native
per-module ESM live bindings).
Real-world impact
Surfaced trying to enable bundledDev on a large app. The same unbound-cyclic-import
pattern broke, in cascade, @sentry/core (class ServerRuntimeClient extends BaseClient
→ BaseClient is not defined; also defineIntegration), @datadog/browser-core
(ONE_SECOND), and first-party code (export const defaultProps = { buttonClass: SelectButton }
across a package cycle). It is widespread enough that per-package workarounds don't scale.
System Info
vite 8.1.0
rolldown 1.1.2 (latest published)
@vitejs/plugin-react 6.0.3 (not required — repro above has no React)
Any additional comments?
Possibly related: #9691 (lazy mode drops an import → node_http2 is not defined).
#9485 (circular import in vite dev) was closed as fixed by #9419, but that was the
CJS-interop variant — this pure-ESM cycle still reproduces on 1.1.2.
Reproduction link or steps
Minimal, self-contained (pure ESM, no CJS/barrel/dynamic-import). 4 files:
vite.config.js
index.html
src/main.js
src/a.js
src/b.js
Run
vite(dev), open the page.What is expected?
Page renders
button=B.Bresolves to the class exported byb.js, as it doesin classic Vite dev (rollup+esbuild) and in
vite build.What is actually happening?
ReferenceError: B is not defined.The served entry chunk
/assets/index.jsemits:…but
class Bis not declared anywhere in that chunk (it lives inb.js'sseparate lazy chunk). So the value-position reference to the cyclic import is
emitted unbound.
Key detail: only imports used during module initialization (top-level) break —
export const x = { button: B },class X extends B,const y = makeThing(B), etc.Sibling imports from the same module that are referenced only inside function
bodies are bound correctly. The unbound one happens to participate in an import
cycle.
This is
experimental.bundledDevonly;experimental.devMode.lazy: falsedoes notchange it. Classic dev avoids it (esbuild optimizeDeps flattens dep cycles + native
per-module ESM live bindings).
Real-world impact
Surfaced trying to enable
bundledDevon a large app. The same unbound-cyclic-importpattern broke, in cascade,
@sentry/core(class ServerRuntimeClient extends BaseClient→
BaseClient is not defined; alsodefineIntegration),@datadog/browser-core(
ONE_SECOND), and first-party code (export const defaultProps = { buttonClass: SelectButton }across a package cycle). It is widespread enough that per-package workarounds don't scale.
System Info
Any additional comments?
Possibly related: #9691 (lazy mode drops an import →
node_http2 is not defined).#9485 (circular import in vite dev) was closed as fixed by #9419, but that was the
CJS-interop variant — this pure-ESM cycle still reproduces on 1.1.2.