#10863·rolldown

[Bug]: dynamic entries sorted by path give an import cycle an execution order that cannot occur at runtime

Author: mshykovCreated Sep 12, 2026Updated Sep 12, 2026

Reproduction link or steps

Four files, no plugins, input + format: esm only.

src/entry.js

javascript
export default { async fetch() { return (await import("./server.js")).server_exports; } };

src/server.js

javascript
import { __exportAll, getRouter } from "./router.js";

export function getStartContext() { return "ctx"; }

async function loadRoutes() {
  const a = await import("./routeA.js");
  return [a.A(), getRouter()];
}

// Module-evaluation-time use of a binding from the other half of the cycle.
export const server_exports = /* @__PURE__ */ __exportAll({ default: () => loadRoutes });

src/router.js

javascript
import { getStartContext } from "./server.js";

// `var`, so a too-late declaration yields `undefined` rather than a TDZ error.
var __exportAll = (all) => {
  const target = {};
  for (const name in all) Object.defineProperty(target, name, { get: all[name], enumerable: true });
  return target;
};

// Deferred: the other half of the cycle is only read when this is called.
export const getRouter = () => `router(${getStartContext()})`;

export { __exportAll };

src/routeA.js

javascript
import { getRouter } from "./router.js";
export const A = () => "A:" + getRouter();

rolldown.config.mjs

javascript
export default { input: "src/entry.js", output: { dir: "out", format: "esm", minifyInternalExports: false } };

Run the source as plain ESM, then the bundle:

bash
node -e "import('./src/entry.js').then(m=>m.default.fetch()).then(r=>console.log('native  : OK',Object.keys(r))).catch(e=>console.log('native  : FAIL',e.message))"
npx rolldown -c rolldown.config.mjs
node -e "import('./out/entry.js').then(m=>m.default.fetch()).then(r=>console.log('bundled : OK',Object.keys(r))).catch(e=>console.log('bundled : FAIL',e.message))"

What is expected?

Both print OK. The source runs correctly as plain ESM, so the bundle should too.

native  : OK [ 'default' ]
bundled : OK [ 'default' ]

What is actually happening?

native  : OK [ 'default' ]
bundled : FAIL __exportAll is not a function

The two modules of the cycle are concatenated in the reverse of the order they can be evaluated in:

javascript
//#region src/server.js
const server_exports = __exportAll({ default: () => loadRoutes });   // call
//#endregion
//#region src/router.js
var __exportAll = (all) => { /* ... */ };                            // declaration

server.js and router.js are a cycle, and it only works if router.js is evaluated first. At runtime that is what happens: server.js is entered first, so its dependency router.js runs before server.js's own body.

LinkStage::new sorts non-user-defined entries by (kind, module id), and the module id is the path:

rust
// crates/rolldown/src/stages/link_stage/mod.rs
rest.sort_by_cached_key(|item| {
  (item.kind, scan_stage_output.module_table.modules[item.idx].id().as_str())
});

routeA.js sorts before server.js alphabetically, so sort_modules starts its walk at routeA.js:

routeA -> router -> server (cycle, already on the stack) -> emit server -> emit router

server.js gets the lower exec order and is emitted first, so the var it calls is hoisted but unassigned.

The chosen order is not one legal option among several — it cannot occur. routeA.js is dynamically imported from inside server.js, so nothing reaches it without evaluating server.js first. Forcing that order by hand fails natively too:

bash
node -e "import('./src/routeA.js').then(() => import('./src/server.js'))"
# TypeError: __exportAll is not a function

Remove either of these and the output is correct:

  • entry.js importing server.js statically instead of dynamically
  • routeA.js not importing anything from router.js

System Info

bash
  System:
    OS: macOS 26.6.2
    CPU: (11) arm64 Apple M3 Pro
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 26.8.1
    npm: 11.19.0
    pnpm: 12.3.4
    bun: 1.4.0
  npmPackages:
    rolldown: 1.2.8 => 1.2.8

Any additional comments?

Not a recent regression: reproduces identically on 1.1.5, 1.2.0, 1.2.3, 1.2.4, 1.2.5, 1.2.6, 1.2.7 and 1.2.8.

How I ran into it. A TanStack Start app built through Nitro, which bundles in two stages: Vite writes chunks into node_modules/.nitro/vite/services/ssr/assets/, then rolldown re-bundles those already-chunked files. Stage 1's output contains exactly this cycle (server-*.js imports __exportAll from router-*.js, router-*.js imports getStartContext back), which is fine under its own entry order. Stage 2 has different entries, ~30 route chunks all sort before server-*.js by filename, and the cycle flips. Downstream reports: nitrojs/nitro#4605 and nitrojs/nitro#4113.

Relation to existing issues, as far as I can tell:

  • #10747 — same failure shape and same stack (TanStack Start + Nitro), but that one is a 1.2.4 → 1.2.5 regression across two chunks that import each other. This one is within a single chunk and predates 1.2.5 by a long way, so I do not think they are the same defect, though a fix for ordering may well touch both.
  • #10673 — cross-chunk evaluation order where either entry load order is legitimate and one of them crashes. Here there is only one possible order and rolldown picks the other one.
  • #10294strictExecutionOrder / onDemandWrapping ledger. This reproduces with no experimental options set.
  • #9993 / #10101 — same helper family (__commonJSMin), fixed; this input is unaffected by that fix.

I have a fix and a regression fixture; PR to follow.