Skip to content

phi-friday/repro-rolldown-chunk-bug

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Rolldown Bug Reproduction: Unexpected chunk splitting with codeSplitting: false + DTS

Bug Summary

When using tsdown (powered by rolldown) with codeSplitting: false and DTS generation enabled via rolldown-plugin-dts, rolldown produces unexpected chunk files containing runtime helpers (__defProp, __exportAll, etc.), violating the codeSplitting: false setting.

Versions

Package Version
tsdown 0.20.3
rolldown (bundled in tsdown) 1.0.0-rc.3
rolldown (direct dep) 1.0.0-rc.6
rolldown-plugin-dts 0.22.2
typescript 5.9.3

Root Cause

  1. rolldown-plugin-dts registers a transform hook that detects entry modules
  2. For each entry module, it calls this.emitFile({ type: "chunk", id: dtsVirtualId }) to emit a virtual DTS entry
  3. Rolldown now sees 2 entries (the real entry + the DTS virtual entry) instead of 1
  4. With multiple entries, rolldown extracts shared runtime helpers (__defProp, __exportAll) into a separate chunk file — even though codeSplitting: false is set
  5. This only affects formats where DTS is enabled (e.g., ESM with dts: true). IIFE with dts: false produces a single file as expected.

Steps to Reproduce

bun install
bun run build

Expected Output

dist/
├── index.mjs        # Single ESM bundle (no chunks)
├── index.d.mts      # DTS file
└── index.iife.js    # IIFE bundle

Only 3 files. The ESM bundle should be self-contained.

Actual Output

dist/
├── chunk-DQk6qfdC.mjs   # ← BUG: unexpected runtime helper chunk
├── index.d.mts
├── index.iife.js
└── index.mjs             # imports from chunk-DQk6qfdC.mjs

4 files. index.mjs contains:

import { t as __exportAll } from "./chunk-DQk6qfdC.mjs";
// ... rest of bundle

And chunk-DQk6qfdC.mjs contains:

var __defProp = Object.defineProperty;
var __exportAll = (all, no_symbols) => {
  let target = {};
  for (var name in all) {
    __defProp(target, name, { get: all[name], enumerable: true });
  }
  if (!no_symbols) {
    __defProp(target, Symbol.toStringTag, { value: "Module" });
  }
  return target;
};
export { __exportAll as t };

Key Observations

  • IIFE format (with dts: false): ✅ Single file, no chunks — correct behavior
  • ESM format (with dts: true): ❌ Chunk file generated — bug
  • The only difference is DTS being enabled, which causes rolldown-plugin-dts to call emitFile({ type: "chunk" })
  • The export * as lib from "./lib" pattern triggers the __exportAll runtime helper, which rolldown then splits into a separate chunk when it sees multiple entries

Workaround

A generateBundle plugin that inlines the runtime chunk back into the entry and deletes it:

const inlineRuntimeChunkPlugin = {
  name: "inline-runtime-chunk",
  generateBundle(_, bundle) {
    // Collect non-entry chunks (runtime helpers)
    const chunks = new Map();
    for (const [fileName, output] of Object.entries(bundle)) {
      if (output.type === "chunk" && !output.isEntry && !output.isDynamicEntry) {
        chunks.set(fileName, output);
      }
    }
    if (chunks.size === 0) return;

    // Inline chunk imports into entry chunks
    for (const output of Object.values(bundle)) {
      if (output.type !== "chunk" || !output.isEntry) continue;
      if (output.fileName.endsWith(".d.ts")) continue;

      let { code } = output;
      for (const [chunkFileName, chunk] of chunks) {
        const escapedName = chunkFileName.split("/").pop().replace(/\./g, "\\.");
        const importRegex = new RegExp(
          `import\\s*\\{[^}]+\\}\\s*from\\s*["'][^"']*${escapedName}["'];?\\n?`
        );
        if (!importRegex.test(code)) continue;

        const body = chunk.code
          .replace(/^\/\*[\s\S]*?\*\/\s*\n?/, "")
          .replace(/\/\/#region[^\n]*\n?/g, "")
          .replace(/\/\/#endregion[^\n]*\n?/g, "")
          .replace(/\n?export\s*\{[^}]*\};?\s*$/m, "")
          .trim();

        code = code.replace(importRegex, `${body}\n`);
      }
      output.code = code;
    }

    // Delete inlined chunk files
    for (const fileName of chunks.keys()) {
      delete bundle[fileName];
    }
  },
};

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages