Skip to content

[Bug]: advancedChunks + CJS + includeDependenciesRecursively: false → cross-chunk wrapped-ESM init cycle (init_X is not a function); strictExecutionOrder does not help #9887

Description

@hyfdev

Reproduction link or steps

A REPL link can't carry the advancedChunks config, and this bug requires it, so here is a self-contained copy-paste reproduction.

package.json

{ "name": "repro", "private": true, "type": "module", "devDependencies": { "rolldown": "1.1.2" } }

shared.js

export const extend = Object.assign

dep.js

import { extend } from "./hub.js" // imports back from the hub chunk -> cross-chunk cycle
export const useDep = () => extend({}, {})

interop.cjs

require("./shared.js")
require("./dep.js")

hub.js

import "./interop.cjs"
export * from "./shared.js"
export * from "./dep.js"

main.js

import { useDep, extend } from "./hub.js"
export const go = () => [useDep, extend]

build.mjs

import { rolldown } from "rolldown"
import { pathToFileURL } from "node:url"
import { rmSync } from "node:fs"

const groups = [
  { name: "dep", test: "dep\\.js$" },
  { name: "hub", test: "(hub|interop|shared)\\." },
]

for (const [label, opts] of [
  ["includeDependenciesRecursively:false", { incdep: false }],
  ["includeDependenciesRecursively:false + strictExecutionOrder:true", { incdep: false, seo: true }],
  ["includeDependenciesRecursively:true  (control)", { incdep: true }],
]) {
  const dir = "dist-" + label.replace(/[^a-z]+/gi, "_")
  rmSync(dir, { recursive: true, force: true })
  const b = await rolldown({ input: { main: "./main.js", hub: "./hub.js" }, platform: "browser", logLevel: "silent" })
  await b.write({
    format: "esm", dir, entryFileNames: "[name].js", chunkFileNames: "[name].js",
    strictExecutionOrder: opts.seo === true,
    advancedChunks: { includeDependenciesRecursively: opts.incdep, groups },
  })
  await b.close()
  try { await import(pathToFileURL(`${process.cwd()}/${dir}/main.js`)); console.log("  OK   ", label) }
  catch (e) { console.log("  THROW", label, "->", e.message) }
}

Run:

npm install
node build.mjs

Output (rolldown 1.1.2):

  THROW includeDependenciesRecursively:false                                 -> init_dep is not a function
  THROW includeDependenciesRecursively:false + strictExecutionOrder:true     -> init_dep is not a function
  OK    includeDependenciesRecursively:true  (control)

What is expected?

Importing the entry runs without error (as it does with includeDependenciesRecursively: true), or rolldown errors at build time instead of emitting code that throws at runtime.

What is actually happening?

TypeError: init_dep is not a function at load.

The hub chunk imports init_dep from the dep chunk and calls it eagerly at module-eval time, but dep imports extend back from the hub chunk, so the two chunks form an import cycle and hub runs before dep has assigned var init_dep = __esmMin(...). The import binding is present — the wrapper is simply called before it is initialized:

// hub chunk (generated)
import { n as init_dep, r as useDep } from "./dep.js";
var require_interop = /* @__PURE__ */ __commonJSMin((() => {
	init_shared();
	init_dep();          // <-- init_dep is still undefined here
}));
...
require_interop();
init_dep();

Notes:

  • output.strictExecutionOrder: true does not fix this variant. It changes the emitted output but the cycle and the premature call remain. (As a control, strictExecutionOrder: true does fix the simpler CJS + advancedChunks order bug in [Bug]: incorrect execution order with CJS + advancedChunks #7449.)
  • includeDependenciesRecursively: true (the default) fixes it — the dependency is pulled into the dependent's chunk, so no cross-chunk cycle forms.
  • Not a regression. Reproduces on rolldown 1.0.2, 1.0.3, 1.1.0, 1.1.1, and 1.1.2 (latest).

Minimal trigger (each ingredient is necessary — removing any one makes the output correct):

  1. a CJS module that require()s the ESM modules (forces __esmMin wrapping + an eager init call);
  2. advancedChunks groups that place a dependency in a different chunk than a module that depends back into the first chunk (a cross-chunk cycle);
  3. includeDependenciesRecursively: false;
  4. an eager init at module-eval time (here, the CJS require_*()).

The namespace-import shape is not required (named imports reproduce it too).

System Info

rolldown 1.1.2 (also 1.0.2 / 1.0.3 / 1.1.0 / 1.1.1)
node 24.x, macOS arm64

Any additional comments?

This is the same failure class as #7449 (incorrect execution order with CJS + advancedChunks) and #8803 / #9630, but specifically the case the documented strictExecutionOrder workaround (#8803) does not cover, because includeDependenciesRecursively: false forces the cross-chunk cycle. includeDependenciesRecursively: false is a meaningful real-world setting (it improves vendor-chunk content/hash stability), so a manual-chunking config with any CJS dependency in a cycle can hit silent, runtime-only breakage with no working escape hatch.

Minimum bar would be to error at build time rather than emit code that throws at load. Filing separately from #7449 since the workaround gap is specific to includeDependenciesRecursively: false; happy to fold it into #7449 if preferred.

Metadata

Metadata

Assignees

Type

No type

Fields

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions