Skip to content

Code splitting can drop entry chunk when entry contains dynamic import() #6

@flowerornament

Description

@flowerornament

Summary

When a Volt entry contains dynamic import(...), the production code-splitting path can emit async chunks but fail to emit/use the real entry chunk. In the observed case, manifest["app.js"].file points at an async chunk (app-auto-render.js) instead of app.js.

This breaks browser entry loading for apps that lazy-load packages, for example a markdown renderer that imports KaTeX auto-render on demand.

Environment

  • volt 0.9.1
  • oxc 0.11.0
  • quickbeam 0.10.6
  • Elixir 1.20.0-rc.4
  • macOS Darwin 25.4.0
  • comparison: Vite 8.0.8, React 19.2.5, KaTeX 0.16.45

Repro

Minimal entry:

const root = document.querySelector("#root")!

root.setAttribute("data-entry", "dynamic-katex-entry")
root.textContent = "entry loaded"

void Promise.all([
  import("katex"),
  import("katex/contrib/auto-render"),
  import("katex/dist/katex.min.css")
]).then(([katex]) => {
  root.setAttribute("data-lazy", "loaded")
  root.innerHTML = katex.default.renderToString("G=(A,E,\\Omega)")
})

Build shape:

Volt.Builder.build(
  entry: app_ts,
  outdir: outdir,
  node_modules: node_modules,
  target: :es2020,
  minify: false,
  sourcemap: false,
  hash: false,
  format: :esm,
  name: "app",
  code_splitting: true
)

Actual

The build returns {:ok, result}, but the manifest maps the entry to an async chunk:

{
  "app-auto-render.js": {
    "file": "app-auto-render.js",
    "src": "app-auto-render.js"
  },
  "app-katex.js": {
    "file": "app-katex.js",
    "src": "app-katex.js"
  },
  "app.js": {
    "file": "app-auto-render.js",
    "src": "app.js"
  }
}

There is no real app.js entry chunk in the output. Loading the manifest entry in the browser loads app-auto-render.js, so the entry code never runs.

With code_splitting: false, the same entry errors with:

Invalid value for option "output.file" - When building multiple chunks, the
"output.dir" option must be used, not "output.file". You may set
`output.inlineDynamicImports` to `true` when using dynamic imports.

Expected

Volt should emit a real entry chunk and map app.js to it:

{
  "app.js": {
    "file": "app.js",
    "src": "app.js"
  }
}

The async chunks should remain separate and be referenced by rewritten dynamic imports.

Likely cause

Volt already builds its own chunk graph before calling OXC.bundle/2 for each output chunk.

However, the entry chunk still contains raw dynamic import(...) expressions when it is passed to OXC. OXC then appears to attempt its own dynamic-import chunking for that single chunk and returns the output.dir / output.file error above.

Volt.Builder.Output.build_chunk_bundles/4 currently drops failed chunk bundles. Since the entry chunk fails, later manifest logic falls back to the first successful async chunk.

Candidate fix

A small local patch fixed the fixture: protect dynamic imports before per-chunk OXC bundling, restore them after bundling, then let Volt's own dynamic-import rewriter map them to generated chunk URLs.

Patch shape:

# before OXC.bundle/2 for each chunk
chunk_js = Rewriter.protect_dynamic_imports(chunk_js)

# after BundleResult.extract/1
code = Rewriter.restore_dynamic_imports(code)

Candidate implementation PR: #7

After applying that patch locally and recompiling Volt, the fixture emitted:

{
  "manifest": {
    "app.js": {
      "file": "app.js",
      "src": "app.js"
    }
  },
  "entry_path": ".../dynamic_katex-volt_code_splitting/app.js",
  "js_files": ["app-auto-render.js", "app-katex.js", "app.js"],
  "markers": {
    "dynamic_katex_entry": true,
    "katex_lazy_import": true
  }
}

Notes

  • Vite 8.0.8 handles the same fixture correctly and emits a real entry chunk plus async KaTeX chunks.
  • Direct Rolldown CLI currently errors on the CSS dynamic import because raw Rolldown no longer bundles CSS directly, but Vite's Rolldown-backed pipeline succeeds.
  • This is related in spirit to React TSX dev imports 404 and production build emits invalid CommonJS-style output #2's "manifest points entry at non-entry output" symptom, but the repro here is specifically dynamic imports/code splitting in Volt 0.9.1.

AI disclosure

This issue was drafted and the local repro/audit patch was prepared with assistance from OpenAI Codex. I reviewed the repro, compared it against the current repository docs/issues, tested the candidate fix locally, and am filing it because the fixture appears to isolate a real Volt code-splitting behavior.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions